diff --git a/client.d.ts b/client.d.ts new file mode 100644 index 0000000..c3ef8ee --- /dev/null +++ b/client.d.ts @@ -0,0 +1 @@ +export * from './dist/client/index' diff --git a/docs/content/1.get-started/1.installation.md b/docs/content/1.get-started/1.installation.md index 26e8f8a..62f9ae3 100644 --- a/docs/content/1.get-started/1.installation.md +++ b/docs/content/1.get-started/1.installation.md @@ -21,6 +21,14 @@ yarn add @trpc/server @trpc/client trpc-nuxt zod :: +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + build: { + transpile: ['trpc-nuxt/client'] + } +}) +``` + #### Why @trpc/server? For implementing tRPC endpoints and routers. diff --git a/docs/content/1.get-started/2.usage/1.simple.md b/docs/content/1.get-started/2.usage/1.simple.md new file mode 100644 index 0000000..cf947d2 --- /dev/null +++ b/docs/content/1.get-started/2.usage/1.simple.md @@ -0,0 +1,118 @@ +--- +title: Simple +description: tRPC-Nuxt provides first class integration with tRPC. +--- + +# Simple Usage + +## 1. Create a tRPC router + +Initialize your tRPC backend using the `initTRPC` function and create your first router. + +::code-group + +```ts [server/trpc/trpc.ts] +/** + * This is your entry point to setup the root configuration for tRPC on the server. + * - `initTRPC` should only be used once per app. + * - We export only the functionality that we use so we can enforce which base procedures should be used + * + * Learn how to create protected base procedures and other things below: + * @see https://trpc.io/docs/v10/router + * @see https://trpc.io/docs/v10/procedures + */ +import { initTRPC } from '@trpc/server' + +const t = initTRPC.create() + +/** + * Unprotected procedure + **/ +export const publicProcedure = t.procedure; + +export const router = t.router; +export const middleware = t.middleware; +``` + +```ts [server/api/trpc/[trpc].ts] +/** + * This is the API-handler of your app that contains all your API routes. + * On a bigger app, you will probably want to split this file up into multiple files. + */ +import { createNuxtApiHandler } from 'trpc-nuxt' +import { publicProcedure, router } from '~/server/trpc/trpc' +import { z } from 'zod' + +export const appRouter = router({ + hello: publicProcedure + // This is the input schema of your procedure + .input( + z.object({ + text: z.string().nullish(), + }), + ) + .query(({ input }) => { + // This is what you're returning to your client + return { + greeting: `hello ${input?.text ?? 'world'}`, + } + }), +}) + +// export only the type definition of the API +// None of the actual implementation is exposed to the client +export type AppRouter = typeof appRouter; + +// export API handler +export default createNuxtApiHandler({ + router: appRouter, + createContext: () => ({}), +}) +``` + +:: + +## 2. Create tRPC client plugin + +Create a strongly-typed plugin using your API's type signature. + +```ts [plugins/client.ts] +import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client' +import type { AppRouter } from '~/server/trpc/routers' + +export default defineNuxtPlugin(() => { + /** + * createTRPCNuxtClient adds a `useQuery` composable + * built on top of `useAsyncData`. + */ + const client = createTRPCNuxtClient({ + links: [ + httpBatchLink({ + url: '/api/trpc', + }), + ], + }) + + return { + provide: { + client, + }, + } +}) +``` + +## 3. Make an API request + +```vue [pages/index.vue] + + + +``` diff --git a/docs/content/1.get-started/2.usage.md b/docs/content/1.get-started/2.usage/2.recommended.md similarity index 57% rename from docs/content/1.get-started/2.usage.md rename to docs/content/1.get-started/2.usage/2.recommended.md index e96eacb..9372a60 100644 --- a/docs/content/1.get-started/2.usage.md +++ b/docs/content/1.get-started/2.usage/2.recommended.md @@ -1,13 +1,11 @@ --- -title: Usage +title: Recommended description: tRPC-Nuxt provides first class integration with tRPC. --- -# Usage +# Recommended Usage -## Recommended file structure - -Recommended but not enforced file structure. This is what you get when starting from [the examples](../main/example-apps.md). +Recommended but not enforced file structure. ```graphql . @@ -24,7 +22,7 @@ Recommended but not enforced file structure. This is what you get when starting │ │ ├── context.ts # <-- create app context │ │ └── trpc.ts # <-- procedure helpers ├── plugins -│ ├── client.ts # <-- tRPC Client as a plugin +│ ├── client.ts # <-- tRPC client plugin └── [..] ``` @@ -35,17 +33,26 @@ Initialize your tRPC backend using the `initTRPC` function and create your first ::code-group ```ts [server/trpc/trpc.ts] +/** + * This is your entry point to setup the root configuration for tRPC on the server. + * - `initTRPC` should only be used once per app. + * - We export only the functionality that we use so we can enforce which base procedures should be used + * + * Learn how to create protected base procedures and other things below: + * @see https://trpc.io/docs/v10/router + * @see https://trpc.io/docs/v10/procedures + */ import { initTRPC } from '@trpc/server' -import { Context } from '@/server/trpc/context' +import { Context } from '~/server/trpc/context' -// Avoid exporting the entire t-object since it's not very -// descriptive and can be confusing to newcomers used to t -// meaning translation in i18n libraries. const t = initTRPC.context().create() -// Base router and procedure helpers -export const router = t.router -export const publicProcedure = t.procedure +/** + * Unprotected procedure + **/ +export const publicProcedure = t.procedure; +export const router = t.router; +export const middleware = t.middleware; ``` ```ts [server/trpc/routers/index.ts] @@ -72,8 +79,8 @@ export type AppRouter = typeof appRouter ```ts [server/api/trpc/[trpc].ts] import { createNuxtApiHandler } from 'trpc-nuxt' -import { appRouter } from '@/server/trpc/routers' -import { createContext } from '@/server/trpc/context' +import { appRouter } from '~/server/trpc/routers' +import { createContext } from '~/server/trpc/context' // export API handler export default createNuxtApiHandler({ @@ -105,39 +112,18 @@ If you need to split your router into several subrouters, you can implement them Create a strongly-typed plugin using your API's type signature. ```ts [plugins/client.ts] -import { httpBatchLink, createTRPCProxyClient } from '@trpc/client' -import type { AppRouter } from '@/server/trpc/routers' -import { FetchError } from 'ofetch' +import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client' +import type { AppRouter } from '~/server/trpc/routers' export default defineNuxtPlugin(() => { - const client = createTRPCProxyClient({ + /** + * createTRPCNuxtClient adds a `useQuery` composable + * built on top of `useAsyncData`. + */ + const client = createTRPCNuxtClient({ links: [ httpBatchLink({ url: '/api/trpc', - - - /** - * Replace regular `fetch` with a `$fetch` from nuxt - * - * During server-side rendering, calling $fetch to fetch your internal API routes - * will directly call the relevant function (emulating the request), - * saving an additional API call. - * - * @see https://nuxt.com/docs/api/utils/dollarfetch - */ - fetch: (input, options) => - globalThis.$fetch.raw(input.toString(), options) - .catch((e) => { - if (e instanceof FetchError && e.response) - return e.response - - throw e - }) - .then(response => ({ - ...response, - json: () => Promise.resolve(response._data), - })), - }), ], }) @@ -150,16 +136,18 @@ export default defineNuxtPlugin(() => { }) ``` -## 3. Make API requests +## 3. Make an API request ```vue [pages/index.vue] ``` diff --git a/docs/content/1.get-started/3.client.md b/docs/content/1.get-started/3.client.md new file mode 100644 index 0000000..2af3d27 --- /dev/null +++ b/docs/content/1.get-started/3.client.md @@ -0,0 +1,53 @@ +--- +title: Client +description: tRPC-Nuxt provides first class integration with tRPC. +--- + +# Nuxt client + +The magic of tRPC is making strongly typed API calls without relying on code generation. With full-stack TypeScript projects, you can directly import types from the server into the client! This is a vital part of how tRPC works. + +## Initialize a tRPC client + +Create a typesafe client via a Nuxt [plugin](https://nuxt.com/docs/guide/directory-structure/plugins) with the `createTRPCNuxtClient` method from `trpc-nuxt/client`, and add a `links` array with a [terminating link](https://trpc.io/docs/links#the-terminating-link). If you want to learn more about tRPC links, check out the docs [here](https://trpc.io/docs/links): + +::alert{type="info"} +`createTRPCNuxtClient` extends [createTRPCProxyClient](https://trpc.io/docs/vanilla#initialize-a-trpc-client) and adds a `useQuery` method built on top of [useAsyncData](https://nuxt.com/docs/api/composables/use-async-data). +:: + +```ts [plugins/client.ts] +import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client' +import type { AppRouter } from '~/server/trpc/routers' + +export default defineNuxtPlugin(() => { + const client = createTRPCNuxtClient({ + links: [ + httpBatchLink({ + url: '/api/trpc', + }), + ], + }) + + return { + provide: { + client, + }, + } +}) +``` + +As you can see, we passed `AppRouter` as a type argument of `createTRPCNuxtClient`. This returns a strongly typed `client` instance, a proxy that mirrors the structure of your `AppRouter` on the client: + +```vue [pages/index.vue] + +``` diff --git a/docs/content/1.get-started/4.links/1.httpLink.md b/docs/content/1.get-started/4.links/1.httpLink.md new file mode 100644 index 0000000..e346599 --- /dev/null +++ b/docs/content/1.get-started/4.links/1.httpLink.md @@ -0,0 +1,58 @@ +--- +title: HTTP Link +description: httpLink is a terminating link that sends a tRPC operation to a tRPC procedure over HTTP. +--- + +# HTTP Link + +`httpLink` is a [terminating link](https://trpc.io/docs/links#the-terminating-link) that sends a tRPC operation to a tRPC procedure over HTTP. + +`httpLink` supports both POST and GET requests. + +::alert{type="info"} +`httpLink` imported from `trpc-nuxt/client` is a convenience wrapper around the original `httpLink` that replaces regular `fetch` with a [`$fetch`](https://nuxt.com/docs/api/utils/dollarfetch) from Nuxt. It also sets the default headers using [`useRequestHeaders`](https://nuxt.com/docs/api/composables/use-request-headers#userequestheaders). +:: + +## Usage + +You can import and add the `httpLink` to the `links` array as such: + +```ts +import { createTRPCNuxtClient, httpLink } from 'trpc-nuxt/client' +import type { AppRouter } from '~/server/trpc/routers' + +const client = createTRPCNuxtClient({ + links: [ + httpLink({ + url: '/api/trpc', + }), + ], +}) +``` + +## `httpLink` Options + +The `httpLink` function takes an options object that has the `HTTPLinkOptions` shape. + +```ts +export interface HTTPLinkOptions { + url: string; + /** + * Select headers to pass to `useRequestHeaders`. + */ + pickHeaders?: string[]; + /** + * Add ponyfill for fetch. + */ + fetch?: typeof fetch; + /** + * Add ponyfill for AbortController + */ + AbortController?: typeof AbortController | null; + /** + * Headers to be set on outgoing requests or a callback that of said headers + * @link http://trpc.io/docs/v10/header + */ + headers?: HTTPHeaders | (() => HTTPHeaders | Promise); +} +``` diff --git a/docs/content/1.get-started/4.links/2.httpBatchLink.md b/docs/content/1.get-started/4.links/2.httpBatchLink.md new file mode 100644 index 0000000..03d4df6 --- /dev/null +++ b/docs/content/1.get-started/4.links/2.httpBatchLink.md @@ -0,0 +1,88 @@ +--- +title: HTTP Batch Link +description: httpBatchLink is a terminating link that batches an array of individual tRPC operations into a single HTTP request that's sent to a single tRPC procedure. +--- + +# HTTP Batch Link + +`httpBatchLink` is a [terminating link](https://trpc.io/docs/links#the-terminating-link) that batches an array of individual tRPC operations into a single HTTP request that's sent to a single tRPC procedure. + +::alert{type="info"} +`httpBatchLink` imported from `trpc-nuxt/client` is a convenience wrapper around the original `httpBatchLink` that replaces regular `fetch` with a [`$fetch`](https://nuxt.com/docs/api/utils/dollarfetch) from Nuxt. It also sets the default headers using [`useRequestHeaders`](https://nuxt.com/docs/api/composables/use-request-headers#userequestheaders). +:: + +## Usage + +You can import and add the `httpBatchLink` to the `links` array as such: + +```ts +import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client' +import type { AppRouter } from '~/server/trpc/routers' + +const client = createTRPCNuxtClient({ + links: [ + httpBatchLink({ + url: '/api/trpc', + }), + ], +}) +``` + +After that, you can make use of batching by setting all your procedures in a `Promise.all`. The code below will produce exactly one HTTP request and on the server exactly `one` database query: + +```ts +const somePosts = await Promise.all([ + $client.post.byId.query(1); + $client.post.byId.query(2); + $client.post.byId.query(3); +]) +``` + +## `httpBatchLink` Options + +The `httpBatchLink` function takes an options object that has the `HTTPBatchLinkOptions` shape. + +```ts +export interface HttpBatchLinkOptions extends HTTPLinkOptions { + maxURLLength?: number; +} + +export interface HTTPLinkOptions { + url: string; + /** + * Select headers to pass to `useRequestHeaders`. + */ + pickHeaders?: string[]; + /** + * Add ponyfill for fetch. + */ + fetch?: typeof fetch; + /** + * Add ponyfill for AbortController + */ + AbortController?: typeof AbortController | null; + /** + * Headers to be set on outgoing requests or a callback that of said headers + * @link http://trpc.io/docs/v10/header + */ + headers?: HTTPHeaders | (() => HTTPHeaders | Promise); +} +``` + +## Setting a maximum URL length + +When sending batch requests, sometimes the URL can become too large causing HTTP errors like [413 Payload Too Large](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413), [414 URI Too Long](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414), and [404 Not Found](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404). The `maxURLLength` option will limit the number of requests that can be sent together in a batch. + +```ts +import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'; +import type { AppRouter } from '~/server/trpc/routers' + +const client = createTRPCNuxtClient({ + links: [ + httpBatchLink({ + url: '/api/trpc', + maxURLLength: 2083, // a suitable size + }), + ], +}); +``` diff --git a/docs/content/1.get-started/3.tips/1.composables.md b/docs/content/1.get-started/5.tips/1.composables.md similarity index 87% rename from docs/content/1.get-started/3.tips/1.composables.md rename to docs/content/1.get-started/5.tips/1.composables.md index 20e7c1a..02929a0 100644 --- a/docs/content/1.get-started/3.tips/1.composables.md +++ b/docs/content/1.get-started/5.tips/1.composables.md @@ -6,6 +6,10 @@ title: Composables It is often useful to wrap functionality of your `@trpc/client` api within other functions. For this purpose, it's necessary to be able to infer input types and output types generated by your `@trpc/server` router. +::alert{type="info"} +[createTRPCNuxtClient](/get-started/client/create) adds a `useQuery` method built on top of [useAsyncData](https://nuxt.com/docs/api/composables/use-async-data/). +:: + ## Inference Helpers `@trpc/server` exports the following helper types to assist with inferring these types from the `AppRouter` exported by your `@trpc/server` router: diff --git a/docs/content/1.get-started/3.tips/2.headers.md b/docs/content/1.get-started/5.tips/2.headers.md similarity index 88% rename from docs/content/1.get-started/3.tips/2.headers.md rename to docs/content/1.get-started/5.tips/2.headers.md index 626840b..1446345 100644 --- a/docs/content/1.get-started/3.tips/2.headers.md +++ b/docs/content/1.get-started/5.tips/2.headers.md @@ -6,6 +6,10 @@ title: Headers We can use the built-in [useRequestHeaders](https://v3.nuxtjs.org/api/composables/use-request-headers/) to set outgoing request headers: +::alert{type="info"} +[createTRPCNuxtClient](/get-started/client/create) has this feature by default. +:: + ```ts [plugins/client.ts] export default defineNuxtPlugin(() => { const headers = useRequestHeaders() diff --git a/docs/content/1.get-started/3.tips/3.authorization.md b/docs/content/1.get-started/5.tips/3.authorization.md similarity index 100% rename from docs/content/1.get-started/3.tips/3.authorization.md rename to docs/content/1.get-started/5.tips/3.authorization.md diff --git a/docs/content/1.get-started/3.tips/4.server-side-calls.md b/docs/content/1.get-started/5.tips/4.server-side-calls.md similarity index 100% rename from docs/content/1.get-started/3.tips/4.server-side-calls.md rename to docs/content/1.get-started/5.tips/4.server-side-calls.md diff --git a/docs/content/1.get-started/3.tips/5.aborting-procedures.md b/docs/content/1.get-started/5.tips/5.aborting-procedures.md similarity index 100% rename from docs/content/1.get-started/3.tips/5.aborting-procedures.md rename to docs/content/1.get-started/5.tips/5.aborting-procedures.md diff --git a/docs/package.json b/docs/package.json index c28b906..52ad93c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,7 +11,7 @@ "nuxt": "^3.0.0" }, "devDependencies": { - "@nuxt-themes/docus": "^0.3.1", - "@nuxtlabs/github-module": "^1.5.3" + "@nuxt-themes/docus": "^1.1.10", + "@nuxtlabs/github-module": "^1.5.4" } } diff --git a/package.json b/package.json index 20a24e7..bb3b417 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "trpc-nuxt", "description": "End-to-end typesafe APIs in Nuxt applications.", "type": "module", + "packageManager": "pnpm@7.18.2", "version": "0.4.3", "license": "MIT", "sideEffects": false, @@ -9,13 +10,19 @@ ".": { "require": "./dist/index.cjs", "import": "./dist/index.mjs" + }, + "./client": { + "types": "./dist/client/index.d.ts", + "require": "./dist/client/index.cjs", + "import": "./dist/client/index.mjs" } }, "main": "./dist/index.mjs", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "files": [ - "dist" + "dist", + "client.d.ts" ], "scripts": { "dev": "concurrently \"pnpm build -- --watch\" \"pnpm --filter playground dev\"", @@ -31,15 +38,15 @@ "@trpc/server": "^10.0.0" }, "dependencies": { - "h3": "^1.0.1", - "nanoid": "^4.0.0", + "h3": "^1.0.2", + "ofetch": "^1.0.0", "ohash": "^1.0.0", "ufo": "^1.0.0" }, "devDependencies": { - "@nuxtjs/eslint-config-typescript": "^11.0.0", - "@trpc/client": "^10.1.0", - "@trpc/server": "^10.1.0", + "@nuxt/eslint-config": "^0.1.1", + "@trpc/client": "^10.5.0", + "@trpc/server": "^10.5.0", "bumpp": "^8.2.1", "concurrently": "^7.5.0", "eslint": "^8.25.0", @@ -48,7 +55,7 @@ }, "eslintConfig": { "extends": [ - "@nuxtjs/eslint-config-typescript" + "@nuxt/eslint-config" ], "rules": { "@typescript-eslint/no-unused-vars": [ @@ -64,5 +71,13 @@ "*.md", "dist", ".output" - ] + ], + "pnpm": { + "overrides": { + "nuxt": "3.0.0" + } + }, + "engines": { + "node": "^16.13.0 || ^18.12.0" + } } diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index af897c5..a9be588 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -1,3 +1,6 @@ // https://v3.nuxtjs.org/api/configuration/nuxt.config export default defineNuxtConfig({ + build: { + transpile: ['trpc-nuxt/client'] + } }) diff --git a/playground/package.json b/playground/package.json index c24b7ef..907d1bb 100644 --- a/playground/package.json +++ b/playground/package.json @@ -9,8 +9,8 @@ "postinstall": "nuxt prepare" }, "dependencies": { - "@trpc/client": "^10.1.0", - "@trpc/server": "^10.1.0", + "@trpc/client": "^10.5.0", + "@trpc/server": "^10.5.0", "superjson": "^1.11.0", "trpc-nuxt": "workspace:*", "zod": "^3.19.1" diff --git a/playground/pages/index.vue b/playground/pages/index.vue index 07ff363..dc208fb 100644 --- a/playground/pages/index.vue +++ b/playground/pages/index.vue @@ -1,8 +1,4 @@