--- id: cors title: Send cookies cross-origin sidebar_label: CORS & Cookies slug: /client/cors --- If your API resides on a different origin than your front-end and you wish to send cookies to it, you will need to enable [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) on your server and send cookies with your requests by providing the option `{credentials: "include"}` to fetch. The arguments provided to the fetch function used by tRPC can be modified as follows. ```ts twoslash title='app.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ httpBatchLink({ url: 'YOUR_SERVER_URL', fetch(url, options) { return fetch(url, { ...options, credentials: 'include', }); }, }), ], }); ``` :::info You also need to enable CORS on your server by modifying your [adapter](/docs/server/adapters), or the HTTP server which fronts your API. The best way to do this varies adapter-by-adapter and based on your hosting infrastructure, and individual adapters generally document this process where applicable. ::: -------------------------------------------------- --- id: headers title: Headers sidebar_label: Headers slug: /client/headers --- The headers option may be used with any of our HTTP links: [`httpBatchLink`](./links/httpBatchLink.md), [`httpBatchStreamLink`](./links/httpBatchStreamLink.md), [`httpLink`](./links/httpLink.md), or [`httpSubscriptionLink`](./links/httpSubscriptionLink.md). `headers` can be both an object or a function. If it's a function it will get called dynamically for every HTTP request. ```ts twoslash title='utils/trpc.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; let token: string; export function setToken(newToken: string) { /** * You can also save the token to cookies, and initialize from * cookies above. */ token = newToken; } export const trpc = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000', /** * Headers will be called on each request. */ headers() { return { Authorization: token, }; }, }), ], }); ``` ### Example with auth login ```ts twoslash title='auth.ts' // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ auth: t.router({ login: t.procedure .input(z.object({ username: z.string(), password: z.string() })) .mutation(() => ({ accessToken: 'token' })), }), }); export type AppRouter = typeof appRouter; // @filename: utils.ts export function setToken(token: string) {} // @filename: auth.ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const trpc = createTRPCClient({ links: [httpBatchLink({ url: 'http://localhost:3000' })] }); import { setToken } from './utils'; // ---cut--- const result = await trpc.auth.login.mutate({ username: 'user', password: 'pass' }); setToken(result.accessToken); ``` The `token` can be whatever you want it to be. It's entirely up to you whether that's just a client-side variable that you update the value of on success or whether you store the token and pull it from local storage. -------------------------------------------------- --- id: httpBatchLink title: HTTP Batch Link sidebar_label: HTTP Batch Link slug: /client/links/httpBatchLink --- `httpBatchLink` is a [**terminating link**](./overview.md#the-terminating-link) that batches an array of individual tRPC operations into a single HTTP request that's sent to a single tRPC procedure. ## Usage You can import and add the `httpBatchLink` to the `links` array as such: ```ts twoslash title="client/index.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000', // transformer, }), ], }); ``` 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 twoslash // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ post: t.router({ byId: t.procedure.input(z.number()).query(({ input }) => ({ id: input, title: `Post ${input}` })), }), }); export type AppRouter = typeof appRouter; // @filename: client.ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const trpc = createTRPCClient({ links: [httpBatchLink({ url: 'http://localhost:3000' })] }); // ---cut--- const somePosts = await Promise.all([ trpc.post.byId.query(1), trpc.post.byId.query(2), trpc.post.byId.query(3), ]); ``` ## `httpBatchLink` Options {#options} The `httpBatchLink` function takes an options object that has the `HTTPBatchLinkOptions` shape. ```ts twoslash type DataTransformerOptions = any; type HTTPHeaders = Record; type Operation = { id: number; type: 'query' | 'mutation' | 'subscription'; path: string; input: unknown }; type NonEmptyArray = [T, ...T[]]; // ---cut--- export interface HTTPBatchLinkOptions extends HTTPLinkOptions { /** * Maximum length of HTTP URL allowed before operations are split into multiple requests * @default Infinity */ maxURLLength?: number; /** * Maximum number of operations allowed in a single batch request * @default Infinity */ maxItems?: number; } export interface HTTPLinkOptions { url: string | URL; /** * Add ponyfill for fetch */ fetch?: typeof fetch; /** * Data transformer * @see https://trpc.io/docs/server/data-transformers **/ transformer?: DataTransformerOptions; /** * Headers to be set on outgoing requests or a callback that of said headers * @see https://trpc.io/docs/client/headers */ headers?: | HTTPHeaders | ((opts: { opList: NonEmptyArray }) => 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 twoslash title="client/index.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000', maxURLLength: 2083, // a suitable size // alternatively, you can make all RPC-calls to be called with POST // methodOverride: 'POST', }), ], }); ``` ## Limiting batch size ### 1. Set a maximum batch size on your server: `maxBatchSize` limits how many operations may be sent in a single batch request. Requests exceeding this limit will be rejected with a `400 Bad Request` error. This can be passed to any tRPC adapter. ```ts title="server.ts" import { createHTTPServer } from '@trpc/server/adapters/standalone'; createHTTPServer({ maxBatchSize: 10, }); ``` or for example if using Next.js: ```ts title='pages/api/trpc/[trpc].ts' export default trpcNext.createNextApiHandler({ maxBatchSize: 10, }); ``` ### 2. Set a matching limit on the client: Use the `maxItems` option on your batch link to ensure the client doesn't exceed the server's limit. This splits large batches into multiple HTTP requests automatically ```ts title="client/index.ts" import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../server'; const client = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000', // 👇 should be the same or lower than the server's maxBatchSize maxItems: 10, }), ], }); ``` ## Disabling request batching ### 1. Disable `batching` on your server: ```ts twoslash title="server.ts" // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: server.ts // ---cut--- import { createHTTPServer } from '@trpc/server/adapters/standalone'; import { appRouter } from './router'; createHTTPServer({ router: appRouter, // 👇 disable batching allowBatching: false, }); ``` or, if you're using Next.js: ```ts twoslash title='pages/api/trpc/[trpc].ts' // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: pages/api/trpc/[trpc].ts // ---cut--- import { createNextApiHandler } from '@trpc/server/adapters/next'; import { appRouter } from '../../../router'; export default createNextApiHandler({ router: appRouter, // 👇 disable batching allowBatching: false, }); ``` ### 2. Replace `httpBatchLink` with [`httpLink`](./httpLink.md) in your tRPC Client ```ts twoslash title="client/index.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ httpLink({ url: 'http://localhost:3000', }), ], }); ``` or, if you're using Next.js: ```tsx twoslash title='utils/trpc.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts // ---cut--- import type { AppRouter } from '../server'; import { httpLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; export const trpc = createTRPCNext({ config() { return { links: [ httpLink({ url: '/api/trpc', }), ], }; }, }); ``` -------------------------------------------------- --- id: httpBatchStreamLink title: HTTP Batch Stream Link sidebar_label: HTTP Batch Stream Link slug: /client/links/httpBatchStreamLink --- `httpBatchStreamLink` is a [**terminating link**](./overview.md#the-terminating-link) that batches an array of individual tRPC operations into a single HTTP request that's sent to a single tRPC procedure (equivalent to [`httpBatchLink`](./httpBatchLink.md)), but doesn't wait for all the responses of the batch to be ready and streams the responses as soon as any data is available. ## Options Options are identical to [`httpBatchLink options`](./httpBatchLink.md#options), with the following addition: | Option | Type | Default | Description | | -------------- | ----------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `streamHeader` | `'trpc-accept'` \| `'accept'` | `'trpc-accept'` | Which header to use to signal the server that the client wants a streaming response. `'accept'` uses the standard `Accept` header instead of the custom `trpc-accept` header, which can avoid CORS preflight for cross-origin streaming queries since `Accept` is a CORS-safelisted header. | ## Usage > All usage and options are identical to [`httpBatchLink`](./httpBatchLink.md). :::note If you require the ability to change/set response headers (which includes cookies) from within your procedures, make sure to use `httpBatchLink` instead! This is due to the fact that `httpBatchStreamLink` does not support setting headers once the stream has begun. [Read more](https://trpc.io/docs/client/links/httpBatchLink). ::: You can import and add the `httpBatchStreamLink` to the `links` array as such: ```ts twoslash title="client/index.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchStreamLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ httpBatchStreamLink({ url: 'http://localhost:3000', }), ], }); ``` 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 twoslash // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ post: t.router({ byId: t.procedure.input(z.number()).query(({ input }) => ({ id: input, title: `Post ${input}` })), }), }); export type AppRouter = typeof appRouter; // @filename: client.ts import { createTRPCClient, httpBatchStreamLink } from '@trpc/client'; import type { AppRouter } from './server'; const trpc = createTRPCClient({ links: [httpBatchStreamLink({ url: 'http://localhost:3000' })] }); // ---cut--- const somePosts = await Promise.all([ trpc.post.byId.query(1), trpc.post.byId.query(2), trpc.post.byId.query(3), ]); ``` ## Streaming mode When batching requests together, the behavior of a regular `httpBatchLink` is to wait for all requests to finish before sending the response. If you want to send responses as soon as they are ready, you can use `httpBatchStreamLink` instead. This is useful for long-running requests. ```ts twoslash title="client/index.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchStreamLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ httpBatchStreamLink({ url: 'http://localhost:3000', }), ], }); ``` Compared to a regular `httpBatchLink`, a `httpBatchStreamLink` will: - Cause the requests to be sent with a `trpc-accept: application/jsonl` header (or `Accept: application/jsonl` when using `streamHeader: 'accept'`) - Cause the response to be sent with a `transfer-encoding: chunked` and `content-type: application/jsonl` - Remove the `data` key from the argument object passed to `responseMeta` (because with a streamed response, the headers are sent before the data is available) ## Async generators and deferred promises {#generators} You can try this out on the homepage of tRPC.io: [https://trpc.io/?try=minimal#try-it-out](/?try=minimal#try-it-out) ```ts twoslash // @target: esnext // @filename: trpc.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create({}); export const router = t.router; export const publicProcedure = t.procedure; // @filename: server.ts // ---cut--- import { publicProcedure, router } from './trpc'; const appRouter = router({ examples: { iterable: publicProcedure.query(async function* () { for (let i = 0; i < 3; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); yield i; } }), }, }); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchStreamLink } from '@trpc/client'; import type { AppRouter } from './server'; const trpc = createTRPCClient({ links: [ httpBatchStreamLink({ url: 'http://localhost:3000', }), ], }); const iterable = await trpc.examples.iterable.query(); // ^? for await (const value of iterable) { console.log('Iterable:', value); // ^? } ``` ## Compatibility (client-side) ### Browsers Browser support should be identical to [`fetch`](https://caniuse.com/fetch) support. ### Node.js / Deno For runtimes other than the browser ones, the `fetch` implementation should support streaming, meaning that the response obtained by `await fetch(...)` should have a `body` property of type `ReadableStream | NodeJS.ReadableStream`, meaning that: - either `response.body.getReader` is a function that returns a `ReadableStreamDefaultReader` object - or `response.body` is a `Uint8Array` `Buffer` This includes support for `undici`, `node-fetch`, native Node.js fetch implementation, and WebAPI fetch implementation (browsers). ### React Native Receiving the stream relies on the `TextDecoder` and `TextDecoderStream` APIs, which are not available in React Native. It's important to note that if your `TextDecoderStream` polyfill does not automatically polyfill `ReadableStream` and `WritableStream` those will also need to be polyfilled. If you still want to enable streaming, you need to polyfill those. You will also need to override the default fetch in the `httpBatchStreamLink` configuration options. In the below example we will be using the [Expo fetch](https://docs.expo.dev/versions/latest/sdk/expo/) package for the fetch implementation. ```ts import { httpBatchStreamLink } from '@trpc/client'; httpBatchStreamLink({ fetch: (url, opts) => fetch(url, { ...opts, reactNative: { textStreaming: true }, }), url: 'http://localhost:3000', }); ``` ## Compatibility (server-side) :::caution AWS Lambda `httpBatchStreamLink` only supported on AWS Lambda when your [infrastructure is set up](/docs/server/adapters/aws-lambda#aws-lambda-response-streaming-adapter) for streaming responses. If not this Link will simply behave like a regular `httpBatchLink`. ::: :::caution Cloudflare Workers You need to enable the `ReadableStream` API through a feature flag: [`streams_enable_constructors`](https://developers.cloudflare.com/workers/platform/compatibility-dates#streams-constructors). ::: ## Reference You can check out the source code for this link on [GitHub.](https://github.com/trpc/trpc/blob/main/packages/client/src/links/httpBatchStreamLink.ts) ## Configure a ping option to keep the connection alive When setting up your root config, you can pass in a `jsonl` option to configure a ping option to keep the connection alive. ```ts twoslash import { initTRPC } from '@trpc/server'; const t = initTRPC.create({ jsonl: { pingMs: 1000, }, }); ``` -------------------------------------------------- --- id: httpLink title: HTTP Link sidebar_label: HTTP Link slug: /client/links/httpLink --- `httpLink` is a [**terminating link**](./overview.md#the-terminating-link) that sends a tRPC operation to a tRPC procedure over HTTP. `httpLink` supports both POST and GET requests. ## Usage You can import and add the `httpLink` to the `links` array as such: ```ts twoslash title="client/index.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ httpLink({ url: 'http://localhost:3000', // transformer, }), ], }); ``` ## `httpLink` Options The `httpLink` function takes an options object that has the `HTTPLinkOptions` shape. ```ts twoslash type DataTransformerOptions = any; type HTTPHeaders = Record; type Operation = { id: number; type: string; input: unknown; path: string }; // ---cut--- export interface HTTPLinkOptions { url: string | URL; /** * Add ponyfill for fetch */ fetch?: typeof fetch; /** * Data transformer * @see https://trpc.io/docs/server/data-transformers **/ transformer?: DataTransformerOptions; /** * Headers to be set on outgoing requests or a callback that of said headers * @see https://trpc.io/docs/client/headers */ headers?: | HTTPHeaders | ((opts: { op: Operation }) => HTTPHeaders | Promise); /** * Send all requests as POSTS requests regardless of the procedure type * The server must separately allow overriding the method. See: * @see https://trpc.io/docs/rpc */ methodOverride?: 'POST'; } ``` ## Reference You can check out the source code for this link on [GitHub.](https://github.com/trpc/trpc/blob/main/packages/client/src/links/httpLink.ts) -------------------------------------------------- --- id: httpSubscriptionLink title: HTTP Subscription Link sidebar_label: HTTP Subscription Link slug: /client/links/httpSubscriptionLink --- `httpSubscriptionLink` is a [**terminating link**](./overview.md#the-terminating-link) that uses [Server-sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) (SSE) for subscriptions. SSE is a good option for real-time as it's a bit easier than setting up a WebSockets-server. ## Setup {#setup} :::info If your client's environment doesn't support EventSource, you need an [EventSource polyfill](https://www.npmjs.com/package/event-source-polyfill). For React Native specific instructions please defer to the [compatibility section](#compatibility-react-native). ::: To use `httpSubscriptionLink`, you need to use a [splitLink](./splitLink.mdx) to make it explicit that we want to use SSE for subscriptions. ```ts twoslash title="client/index.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink, httpSubscriptionLink, loggerLink, splitLink, } from '@trpc/client'; import type { AppRouter } from './server'; const trpcClient = createTRPCClient({ /** * @see https://trpc.io/docs/v11/client/links */ links: [ // adds pretty logs to your console in development and logs errors in production loggerLink(), splitLink({ // uses the httpSubscriptionLink for subscriptions condition: (op) => op.type === 'subscription', true: httpSubscriptionLink({ url: `/api/trpc`, }), false: httpBatchLink({ url: `/api/trpc`, }), }), ], }); ``` :::tip The document here outlines the specific details of using `httpSubscriptionLink`. For general usage of subscriptions, see [our subscriptions guide](../../server/subscriptions.md). ::: ## Headers and authorization / authentication ### Web apps #### Same domain If you're doing a web application, cookies are sent as part of the request as long as your client is on the same domain as the server. #### Cross-domain If the client and server are not on the same domain, you can use `withCredentials: true` ([read more on MDN here](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/withCredentials)). **Example:** ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts import { httpSubscriptionLink } from '@trpc/client'; // ---cut--- // [...] httpSubscriptionLink({ url: 'https://example.com/api/trpc', eventSourceOptions() { return { withCredentials: true, // <--- }; }, }); ``` ### Custom headers through ponyfill **Recommended for non-web environments** You can ponyfill `EventSource` and use the `eventSourceOptions` -callback to populate headers. ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts declare function getSignature(op: any): Promise; // ---cut--- import { createTRPCClient, httpBatchLink, httpSubscriptionLink, splitLink, } from '@trpc/client'; import { EventSourcePolyfill } from 'event-source-polyfill'; import type { AppRouter } from './server'; // Initialize the tRPC client const trpc = createTRPCClient({ links: [ splitLink({ condition: (op) => op.type === 'subscription', true: httpSubscriptionLink({ url: 'http://localhost:3000', // ponyfill EventSource EventSource: EventSourcePolyfill, // options to pass to the EventSourcePolyfill constructor eventSourceOptions: async ({ op }) => { // ^ Includes the operation that's being executed // you can use this to generate a signature for the operation const signature = await getSignature(op); return { headers: { authorization: 'Bearer supersecret', 'x-signature': signature, }, }; }, }), false: httpBatchLink({ url: 'http://localhost:3000', }), }), ], }); ``` ### Updating configuration on an active connection {#updatingConfig} `httpSubscriptionLink` leverages SSE through `EventSource`, ensuring that connections encountering errors like network failures or bad response codes are automatically retried. However, `EventSource` does not allow re-execution of the `eventSourceOptions()` or `url()` options to update its configuration, which is particularly important in scenarios where authentication has expired since the last connection. To address this limitation, you can use a [`retryLink`](./retryLink.md) in conjunction with `httpSubscriptionLink`. This approach ensures that the connection is re-established with the latest configuration, including any updated authentication details. :::caution Please note that restarting the connection will result in the `EventSource` being recreated from scratch, which means any previously tracked events will be lost. ::: ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts declare function getAuthenticatedUri(): string; declare const auth: { getOrRenewToken(): Promise }; // ---cut--- import { createTRPCClient, httpBatchLink, httpSubscriptionLink, retryLink, splitLink, } from '@trpc/client'; import { EventSourcePolyfill, EventSourcePolyfillInit, } from 'event-source-polyfill'; import type { AppRouter } from './server'; // Initialize the tRPC client const trpc = createTRPCClient({ links: [ splitLink({ condition: (op) => op.type === 'subscription', false: httpBatchLink({ url: 'http://localhost:3000', }), true: [ retryLink({ retry: (opts) => { opts.op.type; // ^? will always be 'subscription' since we're in a splitLink const code = opts.error.data?.code; if (!code) { // This shouldn't happen as our httpSubscriptionLink will automatically retry within when there's a non-parsable response console.error('No error code found, retrying', opts); return true; } if (code === 'UNAUTHORIZED' || code === 'FORBIDDEN') { console.log('Retrying due to 401/403 error'); return true; } return false; }, }), httpSubscriptionLink({ url: async () => { // calculate the latest URL if needed... return getAuthenticatedUri(); }, // ponyfill EventSource EventSource: EventSourcePolyfill, eventSourceOptions: async () => { // ...or maybe renew an access token const token = await auth.getOrRenewToken(); return { headers: { authorization: `Bearer ${token}`, }, }; }, }), ], }), ], }); ``` ### Connection params {#connectionParams} In order to authenticate with `EventSource`, you can define `connectionParams` in `httpSubscriptionLink`. This will be sent as part of the URL, which is why other methods are preferred. ```ts twoslash title="server/context.ts" import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'; export const createContext = async (opts: CreateHTTPContextOptions) => { const token = opts.info.connectionParams?.token; // ^? // [... authenticate] return {}; }; export type Context = Awaited>; ``` ```ts twoslash title="client/trpc.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink, httpSubscriptionLink, splitLink, } from '@trpc/client'; import type { AppRouter } from './server'; // Initialize the tRPC client const trpc = createTRPCClient({ links: [ splitLink({ condition: (op) => op.type === 'subscription', true: httpSubscriptionLink({ url: 'http://localhost:3000', connectionParams: async () => { // Will be serialized as part of the URL return { token: 'supersecret', }; }, }), false: httpBatchLink({ url: 'http://localhost:3000', }), }), ], }); ``` ## Timeout Configuration {#timeout} The `httpSubscriptionLink` supports configuring a timeout for inactivity through the `reconnectAfterInactivityMs` option. If no messages (including ping messages) are received within the specified timeout period, the connection will be marked as "connecting" and automatically attempt to reconnect. The timeout configuration is set on the server side when initializing tRPC: ```ts twoslash title="server/trpc.ts" import { initTRPC } from '@trpc/server'; export const t = initTRPC.create({ sse: { client: { reconnectAfterInactivityMs: 3_000, }, }, }); ``` ## Server Ping Configuration {#server-ping} The server can be configured to send periodic ping messages to keep the connection alive and prevent timeout disconnections. This is particularly useful when combined with the `reconnectAfterInactivityMs`-option. ```ts twoslash title="server/trpc.ts" import { initTRPC } from '@trpc/server'; export const t = initTRPC.create({ sse: { // Maximum duration of a single SSE connection in milliseconds // maxDurationMs: 60_000, ping: { // Enable periodic ping messages to keep connection alive enabled: true, // Send ping message every 2s intervalMs: 2_000, }, // client: { // reconnectAfterInactivityMs: 3_000 // } }, }); ``` ## Compatibility (React Native) {#compatibility-react-native} The `httpSubscriptionLink` makes use of the `EventSource` API, Streams API, and `AsyncIterator`s, these are not natively supported by React Native and will have to be ponyfilled. To ponyfill `EventSource` we recommend to use a polyfill that utilizes the networking library exposed by React Native, over using a polyfill that uses the `XMLHttpRequest` API. Libraries that polyfill `EventSource` using `XMLHttpRequest` fail to reconnect after the app has been in the background. Consider using the [rn-eventsource-reborn](https://www.npmjs.com/package/rn-eventsource-reborn) package. The Streams API can be ponyfilled using the [web-streams-polyfill](https://www.npmjs.com/package/web-streams-polyfill) package. `AsyncIterator`s can be polyfilled using the [@azure/core-asynciterator-polyfill](https://www.npmjs.com/package/@azure/core-asynciterator-polyfill) package. ### Installation Install the required polyfills: import { InstallSnippet } from '@site/src/components/InstallSnippet'; Add the polyfills to your project before the link is used (e.g. where you add your TRPCReact.Provider): ```ts title="utils/api.tsx" import '@azure/core-asynciterator-polyfill'; import { RNEventSource } from 'rn-eventsource-reborn'; import { ReadableStream, TransformStream } from 'web-streams-polyfill'; globalThis.ReadableStream = globalThis.ReadableStream || ReadableStream; globalThis.TransformStream = globalThis.TransformStream || TransformStream; ``` Once the ponyfills are added, you can continue setting up the `httpSubscriptionLink` as described in the [setup](#setup) section. ## `httpSubscriptionLink` Options ```ts twoslash type AnyClientTypes = any; type DataTransformerOptions = any; type Operation = any; namespace EventSourceLike { export type AnyConstructor = any; export type InitDictOf = any; } // ---cut--- type HTTPSubscriptionLinkOptions< TRoot extends AnyClientTypes, TEventSource extends EventSourceLike.AnyConstructor = typeof EventSource, > = { /** * The URL to connect to (can be a function that returns a URL) */ url: string | (() => string | Promise); /** * Connection params that are available in `createContext()` * Serialized as part of the URL under the `connectionParams` query parameter */ connectionParams?: | Record | null | (() => | Record | null | Promise | null>); /** * Data transformer * @see https://trpc.io/docs/v11/data-transformers */ transformer?: DataTransformerOptions; /** * EventSource ponyfill */ EventSource?: TEventSource; /** * EventSource options or a callback that returns them */ eventSourceOptions?: | EventSourceLike.InitDictOf | ((opts: { op: Operation; }) => | EventSourceLike.InitDictOf | Promise>); }; ``` ## SSE Options on the server ```ts twoslash export interface SSEStreamProducerOptions { ping?: { /** * Enable ping comments sent from the server * @default false */ enabled: boolean; /** * Interval in milliseconds * @default 1000 */ intervalMs?: number; }; /** * Maximum duration in milliseconds for the request before ending the stream * @default undefined */ maxDurationMs?: number; /** * End the request immediately after data is sent * Only useful for serverless runtimes that do not support streaming responses * @default false */ emitAndEndImmediately?: boolean; /** * Client-specific options - these will be sent to the client as part of the first message * @default {} */ client?: { /** * Timeout and reconnect after inactivity in milliseconds * @default undefined */ reconnectAfterInactivityMs?: number; }; } ``` -------------------------------------------------- --- id: localLink title: Local Link description: A link for direct procedure calls without HTTP overhead --- `localLink` is a [**terminating link**](./overview.md#the-terminating-link) that allows you to make tRPC procedure calls directly in your application without going through HTTP. :::info We have prefixed this as `unstable_` as it's a new API, but you're safe to use it! [Read more](/docs/faq#unstable). ::: ## Usage ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, unstable_localLink } from '@trpc/client'; import type { AppRouter } from './server'; import { appRouter } from './server'; const client = createTRPCClient({ links: [ unstable_localLink({ router: appRouter, createContext: async () => { // Create your context here return {}; }, onError: (opts) => { // Log errors here, similarly to how you would in an API route console.error('Error:', opts.error); }, }), ], }); ``` ## Features - Direct procedure calls without HTTP overhead - Full support for queries, mutations, and subscriptions - Automatic error handling and transformation - Support for abort signals - Type-safe context creation ## Options The `localLink` accepts the following options: ```ts twoslash type AnyRouter = any; type inferRouterContext = any; type inferClientTypes = any; type ErrorHandlerOptions = any; type TransformerOptions = {}; // ---cut--- type LocalLinkOptions = { router: TRouter; createContext: () => Promise>; onError?: (opts: ErrorHandlerOptions>) => void; } & TransformerOptions>; ``` ### router The tRPC router instance to use for procedure calls. ### createContext A function that creates the context for each procedure call. This is called for each request and should return a promise that resolves to the context object. ### onError An optional error handler that is called when an error occurs during a procedure call. It receives the error, operation type, path, input, and context. ### transformer Optional input/output transformers for serialization/deserialization of data. ## Notes - It's recommended to use this link in scenarios where you need direct procedure calls without HTTP - For most client-side applications, you should use the `httpLink` or other HTTP-based links instead - The link supports all tRPC features including queries, mutations, and subscriptions - Error handling and transformation are handled automatically, just like with HTTP-based links -------------------------------------------------- --- id: loggerLink title: Logger Link sidebar_label: Logger Link slug: /client/links/loggerLink --- `loggerLink` is a link that lets you implement a logger for your tRPC client. It allows you to see more clearly what operations are queries, mutations, or subscriptions, their requests, and responses. The link, by default, prints a prettified log to the browser's console. However, you can customize the logging behavior and the way it prints to the console with your own implementations. ## Usage You can import and add the `loggerLink` to the `links` array as such: ```ts twoslash title="client/index.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ /** * The function passed to enabled is an example in case you want to the link to * log to your console in development and only log errors in production */ loggerLink({ enabled: (opts) => (process.env.NODE_ENV === 'development' && typeof window !== 'undefined') || (opts.direction === 'down' && opts.result instanceof Error), }), httpBatchLink({ url: 'http://localhost:3000', }), ], }); ``` ## `loggerLink` Options The `loggerLink` function takes an options object that has the `LoggerLinkOptions` shape: ```ts twoslash type AnyRouter = any; type LogFn = (opts: any) => void; type EnabledFn = (opts: any) => boolean; type ConsoleEsque = { log: (...args: any[]) => void; error: (...args: any[]) => void }; // ---cut--- type LoggerLinkOptions = { logger?: LogFn; /** * It is a function that returns a condition that determines whether to enable the logger. * It is true by default. */ enabled?: EnabledFn; /** * Used in the built-in defaultLogger */ console?: ConsoleEsque; /** * Color mode used in the default logger. * @default typeof window === 'undefined' ? 'ansi' : 'css' */ colorMode?: 'ansi' | 'css' | 'none'; /** * Include context in the log - defaults to false unless colorMode is 'css' */ withContext?: boolean; }; ``` ## Reference You can check out the source code for this link on [GitHub.](https://github.com/trpc/trpc/blob/main/packages/client/src/links/loggerLink.ts) -------------------------------------------------- --- id: overview title: Links Overview sidebar_label: Overview slug: /client/links --- Links enable you to customize the flow of data between the tRPC Client and Server. A link should do only one thing, which can be either a self-contained modification to a tRPC operation (query, mutation, or subscription) or a side-effect based on the operation (such as logging). You can compose links together into an array that you can provide to the tRPC client configuration via the `links` property, which represents a link chain. This means that the tRPC client will execute the links in the order they are added to the `links` array when doing a request and will execute them again in reverse when it's handling a response. Here's a visual representation of the link chain:
tRPC Link Diagram tRPC Link Diagram. Based on Apollo's.
```ts twoslash title='utils/trpc.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client'; import type { AppRouter } from './server'; export const trpc = createTRPCClient({ links: [ loggerLink(), httpBatchLink({ url: 'http://localhost:3000', }), ], }); ``` ## Creating a custom link A link is a function that follows the `TRPCLink` type. Each link is composed of three parts: 1. The link returns a function that has no parameters. This is the setup phase where the link is initialized — it happens once per app and is useful for storing caches or other state. 2. The function in step 1 returns another function that receives an object with two properties: `op` which is the `Operation` that is being executed by the client, and `next` which is the function we use to call the next link down the chain. 3. The function in step 2 returns a final function that returns the `observable` function provided by `@trpc/server`. The `observable` accepts a function that receives an `observer` which helps our link notify the next link up the chain how they should handle the operation result. In this function, we can just return `next(op)` and leave it as is, or we can subscribe to `next`, which enables our link to handle the operation result. ### Example ```tsx twoslash title='utils/customLink.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: customLink.ts // ---cut--- import { TRPCLink } from '@trpc/client'; import { observable } from '@trpc/server/observable'; import type { AppRouter } from './server'; export const customLink: TRPCLink = () => { // here we just got initialized in the app - this happens once per app // useful for storing cache for instance return ({ next, op }) => { // this is when passing the result to the next link // each link needs to return an observable which propagates results return observable((observer) => { console.log('performing operation:', op); const unsubscribe = next(op).subscribe({ next(value) { console.log('we received value', value); observer.next(value); }, error(err) { console.log('we received error', err); observer.error(err); }, complete() { observer.complete(); }, }); return unsubscribe; }); }; }; ``` ### References If you need a more real reference for creating your custom link, you can check out some of the built-in links tRPC provides on [GitHub](https://github.com/trpc/trpc/tree/main/packages/client/src/links). ## The terminating link The **terminating link** is the last link in a link chain. Instead of calling the `next` function, the terminating link is responsible for sending your composed tRPC operation to the tRPC server and returning an `OperationResultEnvelope`. The `links` array that you add to the tRPC client config should have at least one link, and that link should be a terminating link. If `links` don't have a terminating link at the end of them, the tRPC operation will not be sent to the tRPC server. [`httpBatchLink`](./httpBatchLink.md) is the recommended terminating link by tRPC. [`httpLink`](./httpLink.md), [`httpBatchStreamLink`](./httpBatchStreamLink.md), [`httpSubscriptionLink`](./httpSubscriptionLink.md), [`wsLink`](./wsLink.md), and [`localLink`](./localLink.mdx) are other examples of terminating links depending on your needs. ## Managing context As an operation moves along your link chain, it maintains a context that each link can read and modify. This allows links to pass metadata along the chain that other links use in their execution logic. Obtain the current context object and modify it by accessing `op.context`. You can set the context object's initial value for a particular operation by providing the context parameter to the `query` or `useQuery` hook (or `mutation`, `subscription`, etc.). For an example use case, see [Disable batching for certain requests](/docs/client/links/splitLink#disable-batching-for-certain-requests). -------------------------------------------------- --- id: retryLink title: Retry Link sidebar_label: Retry Link slug: /client/links/retryLink --- `retryLink` is a link that allows you to retry failed operations in your tRPC client. It provides a customizable way to handle transient errors, such as network failures or server errors, by automatically retrying the failed requests based on specified conditions. :::tip If you use `@trpc/react-query` you will generally **not** need this link as it's built into the `useQuery()` and the `useMutation()` hooks from `@tanstack/react-query`. ::: ## Usage You can import and add the `retryLink` to the `links` array when creating your tRPC client. This link can be placed before or after other links in your setup, depending on your requirements. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink, retryLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ retryLink({ retry(opts) { if ( opts.error.data && opts.error.data.code !== 'INTERNAL_SERVER_ERROR' ) { // Don't retry on non-500s return false; } if (opts.op.type !== 'query') { // Only retry queries return false; } // Retry up to 3 times return opts.attempts <= 3; }, // Double every attempt, with max of 30 seconds (starting at 1 second) retryDelayMs: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }), httpBatchLink({ url: 'http://localhost:3000', }), ], }); ``` In the example above, we add the `retryLink` before the `httpBatchLink`. The `retry` function is required and defines when to retry. In this example, it will: - Retry the request if the error is a `TRPCClientError` with a status code of 500 or if we couldn't get a valid tRPC error. - Retry the request up to 3 times. ## Options ```ts twoslash type InferrableClientTypes = any; type Operation = { id: number; type: string; input: unknown; path: string }; type TRPCClientError = Error & { data: any }; // ---cut--- interface RetryLinkOptions { /** * The retry function */ retry: (opts: RetryFnOptions) => boolean; /** * The delay between retries in ms (defaults to 0) */ retryDelayMs?: (attempt: number) => number; } interface RetryFnOptions { /** * The operation that failed */ op: Operation; /** * The error that occurred */ error: TRPCClientError; /** * The number of attempts that have been made (including the first call) */ attempts: number; } ``` ## Handling tracked() events When using `retryLink` with subscriptions that use [`tracked()`](../../server/subscriptions.md#tracked), the link will automatically include the last known event ID when retrying. This ensures that when a subscription reconnects, it can resume from where it left off without missing any events. For example, if you're using Server-sent Events (SSE) with `httpSubscriptionLink`, the `retryLink` will automatically handle reconnecting with the last event ID when errors like `401 Unauthorized` occur. -------------------------------------------------- --- id: splitLink title: Split Link sidebar_label: Split Link slug: /client/links/splitLink --- import SplitLinkDiagram from '../../../static/img/split-link-diagram.svg'; `splitLink` is a link that allows you to branch your link chain's execution depending on a given condition. Both the `true` and `false` branches are required. You can provide just one link, or multiple links per branch via an array. It's important to note that when you provide links for `splitLink` to execute, `splitLink` will create an entirely new link chain based on the links you passed. Therefore, you need to use a [**terminating link**](./overview.md#the-terminating-link) if you only provide one link or add the terminating link at the end of the array if you provide multiple links to be executed on a branch. Here's a visual representation of how `splitLink` works:
## Usage Example ### Disable batching for certain requests Let's say you're using `httpBatchLink` as the terminating link in your tRPC client config. This means request batching is enabled in every request. However, if you need to disable batching only for certain requests, you would need to change the terminating link in your tRPC client config dynamically between `httpLink` and `httpBatchLink`. This is a perfect opportunity for `splitLink` to be used: #### 1. Configure client / `utils/trpc.ts` ```ts twoslash title="client/index.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink, httpLink, splitLink, } from '@trpc/client'; import type { AppRouter } from './server'; const url = `http://localhost:3000`; const client = createTRPCClient({ links: [ splitLink({ condition(op) { // check for context property `skipBatch` return Boolean(op.context.skipBatch); }, // when condition is true, use normal request true: httpLink({ url, }), // when condition is false, use batching false: httpBatchLink({ url, }), }), ], }); ``` #### 2. Perform request without batching ```ts twoslash title='client.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({ posts: t.procedure.query(() => []), }); export type AppRouter = typeof appRouter; // @filename: client.ts import { createTRPCClient, httpBatchLink, splitLink, httpLink } from '@trpc/client'; import type { AppRouter } from './server'; const proxy = createTRPCClient({ links: [ splitLink({ condition(op) { return Boolean(op.context.skipBatch); }, true: httpLink({ url: 'http://localhost:3000' }), false: httpBatchLink({ url: 'http://localhost:3000' }), }), ], }); // ---cut--- const postResult = proxy.posts.query(undefined, { context: { skipBatch: true, }, }); ``` or: ```tsx twoslash title='MyComponent.tsx' // @jsx: react-jsx // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({ posts: t.procedure.query(() => [{ id: 1, title: 'Hello' }]), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from './server'; export const trpc = createTRPCReact(); // @filename: MyComponent.tsx // ---cut--- import { trpc } from './trpc'; export function MyComponent() { const postsQuery = trpc.posts.useQuery(undefined, { trpc: { context: { skipBatch: true, }, } }); return (
{JSON.stringify(postsQuery.data ?? null, null, 4)}
) } ``` ## `splitLink` Options The `splitLink` function takes an options object that has three fields: `condition`, `true`, and `false`. ```ts twoslash type AnyRouter = any; type Operation = any; type TRPCLink = any; // ---cut--- declare function splitLink(opts: { condition: (op: Operation) => boolean; /** * The link to execute next if the test function returns `true`. */ true: TRPCLink | TRPCLink[]; /** * The link to execute next if the test function returns `false`. */ false: TRPCLink | TRPCLink[]; }): TRPCLink; ``` ## Reference You can check out the source code for this link on [GitHub.](https://github.com/trpc/trpc/blob/main/packages/client/src/links/splitLink.ts) -------------------------------------------------- --- id: wsLink title: WebSocket Link sidebar_label: WebSocket Link slug: /client/links/wsLink --- `wsLink` is a [**terminating link**](./overview.md#the-terminating-link) that's used when using tRPC's WebSockets Client and Subscriptions, which you can learn more about [here](../../server/subscriptions.md). ## Usage To use `wsLink`, you need to pass it a `TRPCWebSocketClient`, which you can create with `createWSClient`: ```ts twoslash title="client/index.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, createWSClient, wsLink } from '@trpc/client'; import type { AppRouter } from './server'; const wsClient = createWSClient({ url: 'ws://localhost:3000', }); const trpcClient = createTRPCClient({ links: [wsLink({ client: wsClient })], }); ``` ## Authentication / Connection params [See more here](../../server/websockets.md#connection-params) ## `wsLink` / `createWSClient` Options The `wsLink` function requires a `TRPCWebSocketClient` to be passed, which can be configured with the fields defined in `WebSocketClientOptions`: ```ts twoslash type TRPCWebSocketClient = any; type DataTransformerOptions = any; type MaybePromise = T | Promise; type Encoder = any; declare function exponentialBackoff(attemptIndex: number): number; // ---cut--- export interface WebSocketLinkOptions { client: TRPCWebSocketClient; /** * Data transformer * @see https://trpc.io/docs/v11/data-transformers **/ transformer?: DataTransformerOptions; } declare function createWSClient(opts: WebSocketClientOptions): TRPCWebSocketClient; export interface WebSocketClientOptions { /** * The URL to connect to (can be a function that returns a URL) */ url: string | (() => MaybePromise); /** * Connection params that are available in `createContext()` * These are sent as the first message */ connectionParams?: Record | null | (() => MaybePromise | null>); /** * Ponyfill which WebSocket implementation to use */ WebSocket?: typeof WebSocket; /** * The number of milliseconds before a reconnect is attempted. * @default {@link exponentialBackoff} */ retryDelayMs?: typeof exponentialBackoff; /** * Triggered when a WebSocket connection is established */ onOpen?: () => void; /** * Triggered when a WebSocket connection encounters an error */ onError?: (evt?: Event) => void; /** * Triggered when a WebSocket connection is closed */ onClose?: (cause?: { code?: number }) => void; /** * Lazy mode will close the WebSocket automatically after a period of inactivity (no messages sent or received and no pending requests) */ lazy?: { /** * Enable lazy mode * @default false */ enabled: boolean; /** * Close the WebSocket after this many milliseconds * @default 0 */ closeMs: number; }; /** * Send ping messages to the server and kill the connection if no pong message is returned */ keepAlive?: { /** * @default false */ enabled: boolean; /** * Send a ping message every this many milliseconds * @default 5_000 */ intervalMs?: number; /** * Close the WebSocket after this many milliseconds if the server does not respond * @default 1_000 */ pongTimeoutMs?: number; }; /** * Custom encoder for wire encoding (e.g. custom binary formats) * @default jsonEncoder */ experimental_encoder?: Encoder; } ``` ## Reference You can check out the source code for this link on [GitHub.](https://github.com/trpc/trpc/blob/main/packages/client/src/links/wsLink) -------------------------------------------------- --- id: server-actions title: Server Actions sidebar_label: Server Actions slug: /client/nextjs/server-actions --- Server Actions allow you to define functions on the server and call them directly from client components, with the network layer abstracted away by the framework. By defining your server actions using tRPC procedures, you get all of tRPC's built-in features: input validation, authentication and authorization through middlewares, output validation, data transformers, and more. :::info The Server Actions integration uses the `experimental_` prefix and is still under active development. The API may change in future releases. ::: ## Setting up Server Action procedures ### 1. Define a base procedure with `experimental_caller` Use `experimental_caller` on a procedure builder together with `experimental_nextAppDirCaller` to create procedures that can be invoked as plain functions (server actions). The `pathExtractor` option lets you identify procedures by metadata, which is useful for logging and observability since server actions don't have a router path like `user.byId`. ```ts twoslash title='server/trpc.ts' import { initTRPC, TRPCError } from '@trpc/server'; import { experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir'; interface Meta { span: string; } export const t = initTRPC.meta().create(); export const serverActionProcedure = t.procedure.experimental_caller( experimental_nextAppDirCaller({ pathExtractor: ({ meta }) => (meta as Meta)?.span ?? '', }), ); ``` ### 2. Add context via middleware Since server actions don't go through an HTTP adapter, there's no `createContext` to inject context. Instead, use a middleware to provide context such as session data: ```ts twoslash title='server/trpc.ts' // @filename: auth.ts export declare function currentUser(): Promise<{ id: string; name: string } | null>; // @filename: server/trpc.ts // ---cut--- import { initTRPC, TRPCError } from '@trpc/server'; import { experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir'; import { currentUser } from '../auth'; interface Meta { span: string; } export const t = initTRPC.meta().create(); export const serverActionProcedure = t.procedure .experimental_caller( experimental_nextAppDirCaller({ pathExtractor: ({ meta }) => (meta as Meta)?.span ?? '', }), ) .use(async (opts) => { const user = await currentUser(); return opts.next({ ctx: { user } }); }); ``` ### 3. Create a protected action procedure Add an authorization middleware to create a reusable base for actions that require authentication: ```ts twoslash title='server/trpc.ts' // @filename: auth.ts export declare function currentUser(): Promise<{ id: string; name: string } | null>; // @filename: server/trpc.ts import { initTRPC, TRPCError } from '@trpc/server'; import { experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir'; import { currentUser } from '../auth'; interface Meta { span: string; } const t = initTRPC.meta().create(); const serverActionProcedure = t.procedure .experimental_caller( experimental_nextAppDirCaller({ pathExtractor: ({ meta }) => (meta as Meta)?.span ?? '' }), ) .use(async (opts) => { const user = await currentUser(); return opts.next({ ctx: { user } }); }); // ---cut--- export const protectedAction = serverActionProcedure.use((opts) => { if (!opts.ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED', }); } return opts.next({ ctx: { ...opts.ctx, user: opts.ctx.user, // ensures type is non-nullable }, }); }); ``` ## Defining a server action Create a file with the `"use server"` directive and define your action using the procedure builder: ```ts twoslash title='app/_actions.ts' // @filename: server/trpc.ts import { initTRPC, TRPCError } from '@trpc/server'; import { experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir'; interface Meta { span: string; } const t = initTRPC.meta().create(); const serverActionProcedure = t.procedure.experimental_caller( experimental_nextAppDirCaller({ pathExtractor: ({ meta }) => (meta as Meta).span }), ); export const protectedAction = serverActionProcedure; // @filename: app/_actions.ts // ---cut--- 'use server'; import { z } from 'zod'; import { protectedAction } from '../server/trpc'; export const createPost = protectedAction .input( z.object({ title: z.string(), }), ) .mutation(async (opts) => { // opts.ctx.user is typed as non-nullable // opts.input is typed as { title: string } // Create the post... }); ``` Because of `experimental_caller`, the procedure is now a plain async function that can be used as a server action. ## Calling from client components Import the server action and use it in a client component. Server actions work with both the `action` attribute for progressive enhancement and programmatic calls via `onSubmit`: ```tsx twoslash title='app/post-form.tsx' // @jsx: react-jsx // @filename: _actions.ts export declare function createPost(input: { title: string }): Promise; // @filename: app/post-form.tsx // ---cut--- 'use client'; import { createPost } from '../_actions'; export function PostForm() { return (
{ e.preventDefault(); const title = new FormData(e.currentTarget).get('title') as string; await createPost({ title }); }} >
); } ``` ## Adding observability with metadata Use the `.meta()` method to tag actions for logging or tracing. The `span` property from the metadata is passed to `pathExtractor`, so it can be used by observability tools: ```ts twoslash title='app/_actions.ts' // @filename: server/trpc.ts import { initTRPC, TRPCError } from '@trpc/server'; import { experimental_nextAppDirCaller } from '@trpc/server/adapters/next-app-dir'; interface Meta { span: string; } const t = initTRPC.meta().create(); const serverActionProcedure = t.procedure.experimental_caller( experimental_nextAppDirCaller({ pathExtractor: ({ meta }) => (meta as Meta)?.span ?? '' }), ); export const protectedAction = serverActionProcedure; // @filename: app/_actions.ts // ---cut--- 'use server'; import { z } from 'zod'; import { protectedAction } from '../server/trpc'; export const createPost = protectedAction .meta({ span: 'create-post' }) .input( z.object({ title: z.string(), }), ) .mutation(async (opts) => { // ... }); ``` ## When to use Server Actions vs mutations Server Actions are not a replacement for all tRPC mutations. Consider the tradeoffs: - **Use Server Actions** when you want progressive enhancement (forms that work without JavaScript), or when the action doesn't need to update client-side React Query cache. - **Use `useMutation`** when you need to update the client-side cache, show optimistic updates, or manage complex loading/error states in the UI. You can incrementally adopt server actions alongside your existing tRPC API - there's no need to rewrite your entire API. -------------------------------------------------- --- id: setup title: Set up with Next.js App Router sidebar_label: Setup slug: /client/nextjs/app-router-setup --- :::note We recommend reading TanStack React Query's [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr) docs to understand the different types of server rendering and footguns to avoid. ::: ## Recommended file structure ```graphql . ├── src │ ├── app │ │ ├── api │ │ │ └── trpc │ │ │ └── [trpc] │ │ │ └── route.ts # <-- tRPC HTTP handler │ │ ├── layout.tsx # <-- mount TRPCReactProvider │ │ └── page.tsx # <-- server component │ ├── trpc │ │ ├── init.ts # <-- tRPC server init & context │ │ ├── routers │ │ │ ├── _app.ts # <-- main app router │ │ │ ├── post.ts # <-- sub routers │ │ │ └── [..] │ │ ├── client.tsx # <-- client hooks & provider │ │ ├── query-client.ts # <-- shared QueryClient factory │ │ └── server.tsx # <-- server-side caller │ └── [..] └── [..] ``` ## Add tRPC to an existing Next.js App Router project ### 1. Install deps import { InstallSnippet } from '@site/src/components/InstallSnippet'; :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: ### 2. Create a tRPC router Initialize your tRPC backend in `trpc/init.ts` using the `initTRPC` function, and create your first router. We're going to make a simple "hello world" router and procedure here — for deeper information on creating your tRPC API, refer to the [Quickstart guide](/docs/quickstart) and [Backend usage docs](/docs/server/overview). ```ts twoslash title='trpc/init.ts' import { initTRPC } from '@trpc/server'; /** * This context creator accepts `headers` so it can be reused in both * the RSC server caller (where you pass `next/headers`) and the * API route handler (where you pass the request headers). */ export const createTRPCContext = async (opts: { headers: Headers }) => { // const user = await auth(opts.headers); return { userId: 'user_123' }; }; // Avoid exporting the entire t-object // since it's not very descriptive. // For instance, the use of a t variable // is common in i18n libraries. const t = initTRPC .context>>() .create({ /** * @see https://trpc.io/docs/server/data-transformers */ // transformer: superjson, }); // Base router and procedure helpers export const createTRPCRouter = t.router; export const createCallerFactory = t.createCallerFactory; export const baseProcedure = t.procedure; ```
```ts twoslash title='trpc/routers/_app.ts' // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCRouter = t.router; export const baseProcedure = t.procedure; // @filename: trpc/routers/_app.ts // ---cut--- import { z } from 'zod'; import { baseProcedure, createTRPCRouter } from '../init'; export const appRouter = createTRPCRouter({ hello: baseProcedure .input( z.object({ text: z.string(), }), ) .query((opts) => { return { greeting: `hello ${opts.input.text}`, }; }), }); // export type definition of API export type AppRouter = typeof appRouter; ``` ### 3. Create the API route handler With the App Router, use the [fetch adapter](/docs/server/adapters/fetch) to handle tRPC requests. Create a route handler that exports both `GET` and `POST`: ```ts twoslash title='app/api/trpc/[trpc]/route.ts' // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; export const createTRPCContext = async (opts: { headers: Headers }) => { return { userId: 'user_123' }; }; const t = initTRPC.create(); export const createTRPCRouter = t.router; export const baseProcedure = t.procedure; // @filename: trpc/routers/_app.ts import { createTRPCRouter } from '../init'; export const appRouter = createTRPCRouter({}); export type AppRouter = typeof appRouter; // @filename: route.ts // ---cut--- import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { createTRPCContext } from './trpc/init'; import { appRouter } from './trpc/routers/_app'; const handler = (req: Request) => fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext: () => createTRPCContext({ headers: req.headers }), }); export { handler as GET, handler as POST }; ``` :::note App Router uses the [fetch adapter](/docs/server/adapters/fetch) (via `fetchRequestHandler`) rather than the Next.js-specific adapter used by the Pages Router. This is because App Router route handlers are based on the Web standard `Request` and `Response` objects. ::: ### 4. Create a Query Client factory Create a shared file `trpc/query-client.ts` that exports a function that creates a `QueryClient` instance. ```ts twoslash title='trpc/query-client.ts' import { defaultShouldDehydrateQuery, QueryClient, } from '@tanstack/react-query'; import superjson from 'superjson'; export function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 30 * 1000, }, dehydrate: { // serializeData: superjson.serialize, shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', }, hydrate: { // deserializeData: superjson.deserialize, }, }, }); } ``` We're setting a few default options here: - `staleTime`: With SSR, we usually want to set some default staleTime above 0 to avoid refetching immediately on the client. - `shouldDehydrateQuery`: This is a function that determines whether a query should be dehydrated or not. Since the RSC transport protocol supports hydrating promises over the network, we extend the `defaultShouldDehydrateQuery` function to also include queries that are still pending. This will allow us to start prefetching in a server component high up the tree, then consuming that promise in a client component further down. - `serializeData` and `deserializeData` (optional): If you set up a [data transformer](https://trpc.io/docs/server/data-transformers) in the previous step, set this option to make sure the data is serialized correctly when hydrating the query client over the server-client boundary. ### 5. Create a tRPC client for Client Components The `trpc/client.tsx` is the entrypoint when consuming your tRPC API from client components. In here, import the **type definition** of your tRPC router and create typesafe hooks using `createTRPCContext`. We'll also export our context provider from this file. ```tsx twoslash title='trpc/client.tsx' // @jsx: react-jsx // @filename: trpc/query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string() })).query(({ input }) => ({ greeting: `hello ${input.text}` })), }); export type AppRouter = typeof appRouter; // @filename: trpc/client.tsx // ---cut--- 'use client'; // ^-- to make sure we can mount the Provider from a server component import type { QueryClient } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query'; import { createTRPCClient, httpBatchLink } from '@trpc/client'; import { createTRPCContext } from '@trpc/tanstack-react-query'; import { useState } from 'react'; import { makeQueryClient } from './query-client'; import type { AppRouter } from './routers/_app'; export const { TRPCProvider, useTRPC } = createTRPCContext(); let browserQueryClient: QueryClient; function getQueryClient() { if (typeof window === 'undefined') { // Server: always make a new query client return makeQueryClient(); } // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient(); return browserQueryClient; } function getUrl() { const base = (() => { if (typeof window !== 'undefined') return ''; if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; return 'http://localhost:3000'; })(); return `${base}/api/trpc`; } export function TRPCReactProvider( props: Readonly<{ children: React.ReactNode; }>, ) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient(); const [trpcClient] = useState(() => createTRPCClient({ links: [ httpBatchLink({ // transformer: superjson, <-- if you use a data transformer url: getUrl(), }), ], }), ); return ( {props.children} ); } ``` Mount the provider in the root layout of your application: ```tsx twoslash title='app/layout.tsx' // @jsx: react-jsx // @filename: types.d.ts declare module '~/trpc/client' { export function TRPCReactProvider(props: { children: React.ReactNode }): React.JSX.Element; } // @filename: app/layout.tsx // ---cut--- import { TRPCReactProvider } from '~/trpc/client'; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ``` ### 6. Create a tRPC caller for Server Components To prefetch queries from server components, we create a proxy from our router. You can also pass in a client if your router is on a separate server. ```tsx twoslash title='trpc/server.tsx' // @filename: server-only.d.ts export {}; // @filename: init.ts export const createTRPCContext = async (opts: { headers: Headers }) => { return { userId: 'user_123' }; }; // @filename: query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string() })).query(({ input }) => ({ greeting: `hello ${input.text}`, })), }); export type AppRouter = typeof appRouter; // @filename: server.tsx // ---cut--- import 'server-only'; // <-- ensure this file cannot be imported from the client import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; import { headers } from 'next/headers'; import { cache } from 'react'; import { createTRPCContext } from './init'; import { makeQueryClient } from './query-client'; import { appRouter } from './routers/_app'; // IMPORTANT: Create a stable getter for the query client that // will return the same client during the same request. export const getQueryClient = cache(makeQueryClient); export const trpc = createTRPCOptionsProxy({ ctx: async () => createTRPCContext({ headers: await headers(), }), router: appRouter, queryClient: getQueryClient, }); // If your router is on a separate server, pass a client instead: // createTRPCOptionsProxy({ // client: createTRPCClient({ links: [httpLink({ url: '...' })] }), // queryClient: getQueryClient, // }); ``` ### 7. Make API requests You're all set! You can now prefetch queries in server components and consume them in client components. #### Prefetching in a Server Component ```tsx twoslash title='app/page.tsx' // @jsx: react-jsx // @filename: types.d.ts declare module '~/trpc/server' { import { QueryClient } from '@tanstack/react-query'; export function getQueryClient(): QueryClient; export const trpc: { hello: { queryOptions: (input: { text: string }) => { queryKey: any[]; queryFn: () => Promise<{ greeting: string }> }; }; }; } // @filename: app/client-greeting.tsx export function ClientGreeting() { return
Hello
; } // @filename: app/page.tsx // ---cut--- import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; import { getQueryClient, trpc } from '~/trpc/server'; import { ClientGreeting } from './client-greeting'; export default async function Home() { const queryClient = getQueryClient(); void queryClient.prefetchQuery( trpc.hello.queryOptions({ text: 'world', }), ); return ( ); } ``` #### Using data in a Client Component ```tsx twoslash title='app/client-greeting.tsx' // @jsx: react-jsx // @filename: types.d.ts declare module '~/trpc/client' { export function useTRPC(): { hello: { queryOptions: (input: { text: string }) => { queryKey: any[]; queryFn: () => Promise<{ greeting: string }>; }; }; }; } // @filename: app/client-greeting.tsx // ---cut--- 'use client'; // <-- hooks can only be used in client components import { useQuery } from '@tanstack/react-query'; import { useTRPC } from '~/trpc/client'; export function ClientGreeting() { const trpc = useTRPC(); const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' })); if (!greeting.data) return
Loading...
; return
{greeting.data.greeting}
; } ``` :::tip You can create `prefetch` and `HydrateClient` helper functions to make this more concise: ```tsx twoslash title='trpc/server.tsx' // @jsx: react-jsx // @filename: query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: trpc/server.tsx import { cache } from 'react'; import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; import { makeQueryClient } from '../query-client'; import type { TRPCQueryOptions } from '@trpc/tanstack-react-query'; export const getQueryClient = cache(makeQueryClient); // ---cut--- export function HydrateClient(props: { children: React.ReactNode }) { const queryClient = getQueryClient(); return ( {props.children} ); } export function prefetch>>( queryOptions: T, ) { const queryClient = getQueryClient(); if (queryOptions.queryKey[1]?.type === 'infinite') { void queryClient.prefetchInfiniteQuery(queryOptions as any); } else { void queryClient.prefetchQuery(queryOptions); } } ``` Then you can use it like this: ```tsx twoslash // @jsx: react-jsx // @filename: types.d.ts declare module '~/trpc/server' { export function HydrateClient(props: { children: React.ReactNode }): React.JSX.Element; export function prefetch(queryOptions: any): void; export const trpc: { hello: { queryOptions: (input: { text: string }) => any; }; }; } // @filename: app/client-greeting.tsx export function ClientGreeting() { return
Hello
; } // @filename: app/page.tsx // ---cut--- import { HydrateClient, prefetch, trpc } from '~/trpc/server'; import { ClientGreeting } from './client-greeting'; function Home() { prefetch(trpc.hello.queryOptions({ text: 'world' })); return ( ); } ``` ::: ## Leveraging Suspense You may prefer handling loading and error states using Suspense and Error Boundaries. You can do this by using the `useSuspenseQuery` hook. ```tsx twoslash title='app/page.tsx' // @jsx: react-jsx // @filename: types.d.ts declare module '~/trpc/server' { export function HydrateClient(props: { children: React.ReactNode }): React.JSX.Element; export function prefetch(queryOptions: any): void; export const trpc: { hello: { queryOptions: (input?: { text: string }) => any; }; }; } // @filename: app/client-greeting.tsx export function ClientGreeting() { return
Hello
; } // @filename: app/page.tsx // ---cut--- import { HydrateClient, prefetch, trpc } from '~/trpc/server'; import { Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { ClientGreeting } from './client-greeting'; export default async function Home() { prefetch(trpc.hello.queryOptions()); return ( Something went wrong}> Loading...}> ); } ``` ```tsx twoslash title='app/client-greeting.tsx' // @jsx: react-jsx // @filename: types.d.ts declare module '~/trpc/client' { export function useTRPC(): { hello: { queryOptions: (input?: { text: string }) => { queryKey: any[]; queryFn: () => Promise<{ greeting: string }>; }; }; }; } // @filename: app/client-greeting.tsx // ---cut--- 'use client'; import { useSuspenseQuery } from '@tanstack/react-query'; import { useTRPC } from '~/trpc/client'; export function ClientGreeting() { const trpc = useTRPC(); const { data } = useSuspenseQuery(trpc.hello.queryOptions()); return
{data.greeting}
; } ``` ## Getting data in a Server Component If you need access to the data in a server component, we recommend creating a server caller and using it directly. Please note that this method is detached from your query client and does not store the data in the cache. This means that you cannot use the data in a server component and expect it to be available in the client. This is intentional and explained in more detail in the [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#data-ownership-and-revalidation) guide. ```tsx twoslash title='trpc/server.tsx' // @filename: init.ts export const createTRPCContext = async (opts: { headers: Headers }) => { return { userId: 'user_123' }; }; // @filename: routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure .input(z.object({ text: z.string() }).optional()) .query(({ input }) => ({ greeting: `hello ${input?.text ?? 'world'}`, })), }); export type AppRouter = typeof appRouter; // @filename: server.tsx // ---cut--- import { headers } from 'next/headers'; import { createTRPCContext } from './init'; import { appRouter } from './routers/_app'; // ... export const caller = appRouter.createCaller(async () => createTRPCContext({ headers: await headers() }), ); ``` ```tsx twoslash title='app/page.tsx' // @jsx: react-jsx // @filename: types.d.ts declare module '~/trpc/server' { export const caller: { hello: () => Promise<{ greeting: string }>; }; } // @filename: app/page.tsx // ---cut--- import { caller } from '~/trpc/server'; export default async function Home() { const greeting = await caller.hello(); // ^? return
{greeting.greeting}
; } ``` If you **really** need to use the data both on the server as well as inside client components and understand the tradeoffs explained in the [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#data-ownership-and-revalidation) guide, you can use `fetchQuery` instead of `prefetch` to have the data both on the server as well as hydrating it down to the client: ```tsx twoslash title='app/page.tsx' // @jsx: react-jsx // @filename: types.d.ts declare module '~/trpc/server' { import { QueryClient } from '@tanstack/react-query'; export function getQueryClient(): QueryClient; export function HydrateClient(props: { children: React.ReactNode }): React.JSX.Element; export const trpc: { hello: { queryOptions: (input?: { text: string }) => { queryKey: any[]; queryFn: () => Promise<{ greeting: string }>; }; }; }; } // @filename: app/client-greeting.tsx export function ClientGreeting() { return
Hello
; } // @filename: app/page.tsx // ---cut--- import { getQueryClient, HydrateClient, trpc } from '~/trpc/server'; import { ClientGreeting } from './client-greeting'; export default async function Home() { const queryClient = getQueryClient(); const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions()); // Do something with greeting on the server return ( ); } ``` ## Next steps - Learn about [Server Actions](/docs/client/nextjs/server-actions) for defining tRPC-powered server actions - Learn about [queries](/docs/client/react/useQuery) and [mutations](/docs/client/react/useMutation) in client components - Explore [server-side calls](/docs/server/server-side-calls) for more advanced server-side patterns - Check out the [SSE Chat example](https://github.com/trpc/trpc/tree/main/examples/next-sse-chat) for a full App Router example with subscriptions -------------------------------------------------- --- id: overview title: Next.js Integration sidebar_label: Overview slug: /client/nextjs --- ## tRPC + Next.js Next.js makes it easy to build a client and server together in one codebase. tRPC makes it easy to share types between them, ensuring typesafety for your application's data fetching. tRPC provides first-class support for both the **App Router** and the **Pages Router**. Choose the guide that matches your project: ## App Router The recommended approach for new Next.js projects. Uses React Server Components, the [fetch adapter](/docs/server/adapters/fetch), and [`@trpc/tanstack-react-query`](/docs/client/tanstack-react-query). Key features: - **Server Components** - Prefetch data on the server and stream it to the client - **Streaming** - Leverage Next.js streaming for optimal loading performance - **Suspense** - Use `useSuspenseQuery` with Suspense boundaries for loading states **[Get started with App Router →](/docs/client/nextjs/app-router-setup)** ## Pages Router Uses `@trpc/next` which provides a higher-order component (HOC) and integrated hooks for the Pages Router data-fetching patterns. Key features: - **Server-side rendering** - Render pages on the server and hydrate them on the client. Read more about [SSR](/docs/client/nextjs/pages-router/ssr). - **Static site generation** - Prefetch queries on the server and generate static HTML files. Read more about [SSG](/docs/client/nextjs/pages-router/ssg). - **Automatic Provider Wrapping** - `@trpc/next` provides a HOC that wraps your app with the necessary providers automatically. **[Get started with Pages Router →](/docs/client/nextjs/pages-router/setup)** ## Choosing between App Router and Pages Router | | App Router | Pages Router | | ------------------- | --------------------------------------------- | --------------------------------------------------- | | **Recommended for** | New projects | Existing Pages Router projects | | **Data fetching** | Server Components, `prefetchQuery` | `getServerSideProps`, `getStaticProps`, SSR via HOC | | **Server adapter** | [Fetch adapter](/docs/server/adapters/fetch) | [Next.js adapter](/docs/server/adapters/nextjs) | | **Client package** | `@trpc/tanstack-react-query` | `@trpc/next` + `@trpc/react-query` | | **Provider setup** | Manual `QueryClientProvider` + `TRPCProvider` | Automatic via `withTRPC()` HOC | :::tip If you're starting a new project, we recommend the App Router. If you have an existing Pages Router project, the Pages Router integration works well and is fully supported. ::: -------------------------------------------------- --- id: aborting-procedure-calls title: Aborting Procedure Calls sidebar_label: Aborting Procedure Calls slug: /client/nextjs/pages-router/aborting-procedure-calls --- By default, tRPC does not cancel requests on unmount. If you want to opt into this behavior, you can provide `abortOnUnmount` in your configuration callback. ### Globally ```ts twoslash title="client.ts" // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; import type { AppRouter } from './server/routers/_app'; export const trpc = createTRPCNext({ config() { return { links: [ httpBatchLink({ url: '/api/trpc', }), ], abortOnUnmount: true, }; }, }); ``` ### Per-request You may also override this behavior at the request level. ```tsx twoslash title="client.ts" // @jsx: react-jsx // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ post: t.router({ byId: t.procedure.input(z.object({ id: z.string() })).query(() => ({ id: '1', title: 'Hello' })), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/routers/_app'; export const trpc = createTRPCReact(); // @filename: client.ts // ---cut--- import { trpc } from './utils/trpc'; import { useRouter } from 'next/router'; function PostViewPage() { const id = useRouter().query.id as string; const postQuery = trpc.post.byId.useQuery({ id }, { trpc: { abortOnUnmount: true } }); return null; } ``` -------------------------------------------------- --- id: server-side-helpers title: Server-Side Helpers sidebar_label: Server-Side Helpers slug: /client/nextjs/pages-router/server-side-helpers --- The server-side helpers provide you with a set of helper functions that you can use to prefetch queries on the server. This is useful for SSG, but also for SSR if you opt not to use `ssr: true`. Prefetching via the server-side helpers allows populating the query cache on the server, which means that these queries do not have to fetch on the client initially. ## There are 2 ways to use the server-side helpers. ### 1. Internal router This method is used when you have direct access to your tRPC router, e.g., when developing a monolithic Next.js application. Using the helpers makes tRPC call your procedures directly on the server, without an HTTP request, similar to [server-side calls](/docs/server/server-side-calls). That also means that you don't have the request and response at hand like you usually do. Make sure you're instantiating the server-side helpers with a context without `req` & `res`, which are typically filled via the context creation. We recommend the concept of ["inner" and "outer" context](/docs/server/context) in that scenario. ```ts twoslash // @module: esnext // @target: es2017 // @filename: server/context.ts export declare function createContext(): Promise<{}>; // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; import superjson from 'superjson'; const t = initTRPC.create({ transformer: superjson }); export const appRouter = t.router({}); // @filename: example.ts // ---cut--- import { createServerSideHelpers } from '@trpc/react-query/server'; import { createContext } from './server/context'; import { appRouter } from './server/routers/_app'; import superjson from 'superjson'; const helpers = createServerSideHelpers({ router: appRouter, ctx: await createContext(), transformer: superjson, }); ``` ### 2. External router This method is used when you don't have direct access to your tRPC router, e.g., when developing a Next.js application and a standalone API hosted separately. ```ts twoslash // @filename: server/router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import { createServerSideHelpers } from '@trpc/react-query/server'; import type { AppRouter } from './server/router'; import superjson from 'superjson'; const proxyClient = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000/api/trpc', }), ], }); const helpers = createServerSideHelpers({ client: proxyClient, }); ``` ## Helpers usage The server-side helpers methods return an object that mirrors your router structure, with all of your routers as keys. However, rather than `useQuery` and `useMutation`, you get `prefetch`, `fetch`, `prefetchInfinite`, and `fetchInfinite` functions. The primary difference between `prefetch` and `fetch` is that `fetch` acts much like a normal function call, returning the result of the query, whereas `prefetch` does not return the result and never throws - if you need that behavior, use `fetch` instead. Instead, `prefetch` will add the query to the cache, which you then dehydrate and send to the client. ```ts twoslash // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: example.ts import { createServerSideHelpers } from '@trpc/react-query/server'; import { appRouter } from './server/routers/_app'; declare const helpers: Awaited>>; // ---cut--- // In getServerSideProps / getStaticProps: const props = { // very important - use `trpcState` as the key trpcState: helpers.dehydrate(), }; ``` The rule of thumb is `prefetch` for queries that you know you'll need on the client, and `fetch` for queries that you want to use the result of on the server. The functions are all wrappers around react-query functions. Please check out [their docs](https://tanstack.com/query/v5/docs/framework/react/overview) to learn more about them in detail. :::info For a full example, see our [E2E SSG test example](https://github.com/trpc/trpc/tree/main/examples/.test/ssg) ::: ## Next.js Example ```tsx twoslash title='pages/posts/[id].tsx' // @jsx: react-jsx // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; import superjson from 'superjson'; const t = initTRPC.create({ transformer: superjson }); export const appRouter = t.router({ post: t.router({ byId: t.procedure .input(z.object({ id: z.string() })) .query(() => ({ id: '1', title: 'Example Post', text: 'Hello world', createdAt: new Date(), })), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/routers/_app'; export const trpc = createTRPCReact(); // @filename: page.tsx // ---cut--- import { createServerSideHelpers } from '@trpc/react-query/server'; import { appRouter } from './server/routers/_app'; import { trpc } from './utils/trpc'; import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import superjson from 'superjson'; export async function getServerSideProps( context: GetServerSidePropsContext<{ id: string }>, ) { const helpers = createServerSideHelpers({ router: appRouter, ctx: {}, transformer: superjson, }); const id = context.params?.id as string; /* * Prefetching the `post.byId` query. * `prefetch` does not return the result and never throws - if you need that behavior, use `fetch` instead. */ await helpers.post.byId.prefetch({ id }); // Make sure to return { props: { trpcState: helpers.dehydrate() } } return { props: { trpcState: helpers.dehydrate(), id, }, }; } export default function PostViewPage( props: InferGetServerSidePropsType, ) { const { id } = props; const postQuery = trpc.post.byId.useQuery({ id }); if (postQuery.status !== 'success') { // won't happen since the query has been prefetched return <>Loading...; } const { data } = postQuery; return ( <>

{data.title}

Created {data.createdAt.toLocaleDateString()}

{data.text}

Raw data:

{JSON.stringify(data, null, 4)}
); } ``` -------------------------------------------------- --- id: setup title: Set up with Next.js Pages Router sidebar_label: Setup slug: /client/nextjs/pages-router/setup --- import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs'; :::caution This guide is for the **Next.js Pages Router**. If you are using the **Next.js App Router**, see the [App Router setup guide](/docs/client/nextjs/app-router-setup) instead. ::: ## Recommended file structure We recommend a file structure like this one, although it is not enforced by tRPC. This is what you'll see in [our examples](/docs/example-apps). The rest of this page will take you through the process of adding tRPC in to this structure. ```graphql . ├── prisma # <-- if prisma is added │ └── [..] ├── src │ ├── pages │ │ ├── _app.tsx # <-- add `withTRPC()`-HOC here │ │ ├── api │ │ │ └── trpc │ │ │ └── [trpc].ts # <-- tRPC HTTP handler │ │ └── [..] │ ├── server │ │ ├── routers │ │ │ ├── _app.ts # <-- main app router │ │ │ ├── post.ts # <-- sub routers │ │ │ └── [..] │ │ ├── context.ts # <-- create app context │ │ └── trpc.ts # <-- procedure helpers │ └── utils │ └── trpc.ts # <-- your typesafe tRPC hooks └── [..] ``` ## Add tRPC to existing Next.js project ### 1. Install deps import { InstallSnippet } from '@site/src/components/InstallSnippet'; :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: The Next.js integration is actually a combination of our [React Query Integration](../../react/overview.mdx) and some Next.js specific integrations. ### 2. Enable strict mode If you want to use Zod for input validation, make sure you have enabled strict mode in your `tsconfig.json`: ```diff title="tsconfig.json" "compilerOptions": { + "strict": true } ``` If strict mode is too harsh, you'll at least want to enable `strictNullChecks`: ```diff title="tsconfig.json" "compilerOptions": { + "strictNullChecks": true } ``` ### 3. Create a tRPC router Initialize your tRPC backend in `src/server/trpc.ts` using the `initTRPC` function, and create your first router. We're going to make a simple "hello world" router and procedure here - but for deeper information on creating your tRPC API you should refer to: - the [Quickstart guide](/docs/quickstart) and [Backend usage docs](/docs/server/overview) for tRPC information - the [Next.js Adapter docs](/docs/server/adapters/nextjs) for mounting tRPC within your Next.js server.
View sample backend ```ts twoslash title='server/trpc.ts' import { initTRPC } from '@trpc/server'; // Avoid exporting the entire t-object // since it's not very descriptive. // For instance, the use of a t variable // is common in i18n libraries. const t = initTRPC.create(); // Base router and procedure helpers export const router = t.router; export const procedure = t.procedure; ```
```ts twoslash title='server/routers/_app.ts' // @filename: server/trpc.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const router = t.router; export const procedure = t.procedure; // @filename: server/routers/_app.ts // ---cut--- import { z } from 'zod'; import { procedure, router } from '../trpc'; export const appRouter = router({ hello: procedure .input( z.object({ text: z.string(), }), ) .query((opts) => { return { greeting: `hello ${opts.input.text}`, }; }), }); // export type definition of API export type AppRouter = typeof appRouter; ```
```ts twoslash title='pages/api/trpc/[trpc].ts' // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: pages/api/trpc/[trpc].ts // ---cut--- import * as trpcNext from '@trpc/server/adapters/next'; import { appRouter } from '../../../server/routers/_app'; // export API handler // @link https://trpc.io/docs/server/adapters export default trpcNext.createNextApiHandler({ router: appRouter, createContext: () => ({}), }); ```
:::note The backend above is using the [recommended file structure](#recommended-file-structure), but you can keep it simple and put everything in [an API handler directly](https://github.com/trpc/trpc/blob/main/examples/next-minimal-starter/src/pages/api/trpc/%5Btrpc%5D.ts) if you prefer. ::: ### 4. Create tRPC hooks Use the `createTRPCNext` function to create a set of strongly-typed hooks from your API's type signature. ```tsx twoslash title='utils/trpc.ts' // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts // ---cut--- import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; import type { AppRouter } from '../server/routers/_app'; function getBaseUrl() { if (typeof window !== 'undefined') // browser should use relative path return ''; if (process.env.VERCEL_URL) // reference for vercel.com return `https://${process.env.VERCEL_URL}`; if (process.env.RENDER_INTERNAL_HOSTNAME) // reference for render.com return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; // assume localhost return `http://localhost:${process.env.PORT ?? 3000}`; } export const trpc = createTRPCNext({ config(config) { return { links: [ httpBatchLink({ /** * If you want to use SSR, you need to use the server's full URL * @see https://trpc.io/docs/client/nextjs/pages-router/ssr **/ url: `${getBaseUrl()}/api/trpc`, // You can pass any HTTP headers you wish here async headers() { return { // authorization: getAuthCookie(), }; }, }), ], }; }, /** * @see https://trpc.io/docs/client/nextjs/pages-router/ssr **/ ssr: false, }); ``` :::note `createTRPCNext` does not work with the tRPC-v9 interop mode. If you are migrating from v9 using interop, you should continue using [the old way of initializing tRPC](../../../../versioned_docs/version-9.x/nextjs/introduction.md#4-create-trpc-hooks). ::: ### 5. Configure `_app.tsx` Wrap your root app page in the `trpc.withTRPC` HOC, similar to this: ```tsx twoslash title='pages/_app.tsx' // @jsx: react-jsx // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; import type { AppRouter } from '../server/routers/_app'; export const trpc = createTRPCNext({ config() { return { links: [httpBatchLink({ url: '/api/trpc' })] }; }, ssr: false, }); // @filename: pages/_app.tsx // ---cut--- import type { AppType } from 'next/app'; import { trpc } from '../utils/trpc'; const MyApp: AppType = ({ Component, pageProps }) => { return ; }; export default trpc.withTRPC(MyApp); ``` ### 6. Make an API request You're all set! You can now use the React hooks you have just created to invoke your API. For more detail see the [React Query Integration](../../react/setup.mdx) ```tsx twoslash title='pages/index.tsx' // @jsx: react-jsx // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string() })).query(({ input }) => ({ greeting: `hello ${input.text}` })), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; import type { AppRouter } from '../server/routers/_app'; export const trpc = createTRPCNext({ config() { return { links: [httpBatchLink({ url: '/api/trpc' })] }; }, ssr: false, }); // @filename: pages/index.tsx // ---cut--- import { trpc } from '../utils/trpc'; export default function IndexPage() { const hello = trpc.hello.useQuery({ text: 'client' }); if (!hello.data) { return
Loading...
; } return (

{hello.data.greeting}

); } ``` ## `createTRPCNext()` options ### `config`-callback The `config`-argument is a function that returns an object that configures the tRPC and React Query clients. This function receives an object with an optional `ctx` property (of type `NextPageContext`) that gives you access to the Next.js `req` object during server-side rendering. The returned value can contain the following properties: - **Required**: - `links` to customize the flow of data between tRPC Client and the tRPC Server. [Read more](/docs/client/links). - Optional: - `queryClientConfig`: a configuration object for the React Query `QueryClient` used internally by the tRPC React hooks: [QueryClient docs](https://tanstack.com/query/v5/docs/reference/QueryClient) - `queryClient`: a React Query [QueryClient instance](https://tanstack.com/query/v5/docs/reference/QueryClient) - **Note:** You can only provide either a `queryClient` or a `queryClientConfig`. - `transformer`: a transformer applied to outgoing payloads. Read more about [Data Transformers](/docs/server/data-transformers) - `abortOnUnmount`: determines if in-flight requests will be cancelled on component unmount. This defaults to `false`. ### `overrides`: (default: `undefined`) {#overrides} Configure [overrides for React Query's hooks](/docs/client/react/useUtils#invalidate-full-cache-on-every-mutation). ### `ssr`-boolean (default: `false`) Whether tRPC should await queries when server-side rendering a page. Defaults to `false`. ### `responseMeta`-callback Ability to set request headers and HTTP status when server-side rendering. #### Example ```tsx twoslash title='utils/trpc.ts' // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts // ---cut--- import { createTRPCNext } from '@trpc/next'; import type { AppRouter } from '../server/routers/_app'; export const trpc = createTRPCNext({ config(config) { return { links: [ /* [...] */ ], }; }, }); ``` ## Next steps Browse the rest of the docs to learn more about things like [authorization](/docs/server/authorization), [middlewares](/docs/server/middlewares), and [error handling](/docs/server/error-handling). You can also find information about [queries](/docs/client/react/useQuery) and [mutations](/docs/client/react/useMutation) now that you're using `@trpc/react-query`. -------------------------------------------------- --- id: ssg title: Static Site Generation sidebar_label: Static Site Generation (SSG) slug: /client/nextjs/pages-router/ssg --- :::tip Reference project: https://github.com/trpc/examples-next-prisma-todomvc ::: Static site generation requires executing tRPC queries inside `getStaticProps` on each page. This can be done using [server-side helpers](/docs/client/nextjs/pages-router/server-side-helpers) to prefetch the queries, dehydrate them, and pass it to the page. The queries will then automatically pick up the `trpcState` and use it as an initial value. ## Fetch data in `getStaticProps` ```tsx twoslash title='pages/posts/[id].tsx' // @jsx: react-jsx // @filename: server/context.ts export declare const prisma: { post: { findMany: (opts: { select: { id: true } }) => Promise<{ id: string }[]>; }; }; // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; import superjson from 'superjson'; const t = initTRPC.create({ transformer: superjson }); export const appRouter = t.router({ post: t.router({ byId: t.procedure .input(z.object({ id: z.string() })) .query(() => ({ id: '1', title: 'Example Post', text: 'Hello world', createdAt: new Date(), })), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/routers/_app'; export const trpc = createTRPCReact(); // @filename: page.tsx // ---cut--- import { createServerSideHelpers } from '@trpc/react-query/server'; import { prisma } from './server/context'; import { appRouter } from './server/routers/_app'; import { trpc } from './utils/trpc'; import { GetStaticPaths, GetStaticPropsContext, InferGetStaticPropsType, } from 'next'; import superjson from 'superjson'; export async function getStaticProps( context: GetStaticPropsContext<{ id: string }>, ) { const helpers = createServerSideHelpers({ router: appRouter, ctx: {}, transformer: superjson, // optional - adds superjson serialization }); const id = context.params?.id as string; // prefetch `post.byId` await helpers.post.byId.prefetch({ id }); return { props: { trpcState: helpers.dehydrate(), id, }, revalidate: 1, }; } export const getStaticPaths: GetStaticPaths = async () => { const posts = await prisma.post.findMany({ select: { id: true, }, }); return { paths: posts.map((post) => ({ params: { id: post.id, }, })), // https://nextjs.org/docs/pages/api-reference/functions/get-static-paths#fallback-blocking fallback: 'blocking', }; }; export default function PostViewPage( props: InferGetStaticPropsType, ) { const { id } = props; const postQuery = trpc.post.byId.useQuery({ id }); if (postQuery.status !== 'success') { // won't happen since we're using `fallback: "blocking"` return <>Loading...; } const { data } = postQuery; return ( <>

{data.title}

Created {data.createdAt.toLocaleDateString('en-us')}

{data.text}

Raw data:

{JSON.stringify(data, null, 4)}
); } ``` Note that the default behaviour of `react-query` is to refetch the data on the client-side when it mounts, so if you want to _only_ fetch the data via `getStaticProps`, you need to set `refetchOnMount` and `refetchOnWindowFocus` to `false` in the query options. This might be preferable if you want to minimize the number of requests to your API, which might be necessary if you're using a third-party rate-limited API for example. This can be done per query: ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const appRouter = t.router({ example: t.procedure.query(() => 'hello'), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: component.tsx // ---cut--- import { trpc } from './utils/trpc'; const data = trpc.example.useQuery( // if your query takes no input, make sure that you don't // accidentally pass the query options as the first argument undefined, { refetchOnMount: false, refetchOnWindowFocus: false }, ); ``` Or globally, if every query across your app should behave the same way: ```tsx twoslash title='utils/trpc.ts' // @filename: utils/api/trpc/[trpc].ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts declare function getBaseUrl(): string; // ---cut--- import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; import superjson from 'superjson'; import type { AppRouter } from './api/trpc/[trpc]'; export const trpc = createTRPCNext({ config(config) { return { links: [ httpBatchLink({ url: `${getBaseUrl()}/api/trpc`, }), ], // Change options globally queryClientConfig: { defaultOptions: { queries: { refetchOnMount: false, refetchOnWindowFocus: false, }, }, }, }; }, }); ``` Be careful with this approach if your app has a mixture of static and dynamic queries. -------------------------------------------------- --- id: ssr title: Server-Side Rendering sidebar_label: Server-Side Rendering (SSR) slug: /client/nextjs/pages-router/ssr --- To enable SSR just set `ssr: true` in your `createTRPCNext` config callback. :::info When you enable SSR, tRPC will use `getInitialProps` to prefetch all queries on the server. This results in problems [like this](https://github.com/trpc/trpc/issues/596) when you use `getServerSideProps`, and solving it is out of our hands.   Alternatively, you can leave SSR disabled (the default) and use [Server-Side Helpers](server-side-helpers) to prefetch queries in `getStaticProps` or `getServerSideProps`. ::: In order to execute queries properly during the server-side render step, we need to add extra logic inside our `config`. Additionally, consider [`Response Caching`](../../../server/caching.md). ```tsx twoslash title='utils/trpc.ts' // @filename: utils/api/trpc/[trpc].ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts declare function getBaseUrl(): string; // ---cut--- import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; import { ssrPrepass } from '@trpc/next/ssrPrepass'; import type { AppRouter } from './api/trpc/[trpc]'; export const trpc = createTRPCNext({ ssr: true, ssrPrepass, config(info) { const { ctx } = info; if (typeof window !== 'undefined') { // during client requests return { links: [ httpBatchLink({ url: '/api/trpc', }), ], }; } return { links: [ httpBatchLink({ // The server needs to know your app's full url url: `${getBaseUrl()}/api/trpc`, /** * Set custom request headers on every request from tRPC * @see https://trpc.io/docs/client/headers */ headers() { if (!ctx?.req?.headers) { return {}; } // To use SSR properly, you need to forward client headers to the server // This is so you can pass through things like cookies when we're server-side rendering return { cookie: ctx.req.headers.cookie, }; }, }), ], }; }, }); ``` or, if you want to SSR conditional on a given request, you can pass a callback to `ssr`. This callback can return a boolean, or a Promise resolving to a boolean: ```tsx twoslash title='utils/trpc.ts' // @filename: utils/api/trpc/[trpc].ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts declare function getBaseUrl(): string; // ---cut--- import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; import { ssrPrepass } from '@trpc/next/ssrPrepass'; import type { AppRouter } from './api/trpc/[trpc]'; export const trpc = createTRPCNext({ ssrPrepass, config(info) { const { ctx } = info; if (typeof window !== 'undefined') { // during client requests return { links: [ httpBatchLink({ url: '/api/trpc', }), ], }; } return { links: [ httpBatchLink({ // The server needs to know your app's full url url: `${getBaseUrl()}/api/trpc`, /** * Set custom request headers on every request from tRPC * @see https://trpc.io/docs/client/headers */ headers() { if (!ctx?.req?.headers) { return {}; } // To use SSR properly, you need to forward client headers to the server // This is so you can pass through things like cookies when we're server-side rendering return { cookie: ctx.req.headers.cookie, }; }, }), ], }; }, ssr(opts) { // only SSR if the request is coming from a bot return opts.ctx?.req?.headers['user-agent']?.includes('bot') ?? false; }, }); ``` ```tsx twoslash title='pages/_app.tsx' // @jsx: react-jsx // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCNext } from '@trpc/next'; import { httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../server/routers/_app'; export const trpc = createTRPCNext({ config() { return { links: [httpBatchLink({ url: '/api/trpc' })] }; }, }); // @filename: pages/_app.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; import type { AppProps } from 'next/app'; import type { AppType } from 'next/app'; const MyApp: AppType = ({ Component, pageProps }: AppProps) => { return ; }; export default trpc.withTRPC(MyApp); ``` ## Response Caching with SSR If you turn on SSR in your app, you might discover that your app loads slowly on, for instance, Vercel, but you can actually statically render your whole app without using SSG; [read this Twitter thread](https://twitter.com/alexdotjs/status/1386274093041950722) for more insights. You can use the `responseMeta` callback on `createTRPCNext` to set cache headers for SSR responses. See also the general [Response Caching](../../../server/caching.md) docs for framework-agnostic caching with `responseMeta`. ```tsx twoslash title='utils/trpc.tsx' // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx // ---cut--- import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; import { ssrPrepass } from '@trpc/next/ssrPrepass'; import type { AppRouter } from '../server/routers/_app'; export const trpc = createTRPCNext({ config() { if (typeof window !== 'undefined') { return { links: [ httpBatchLink({ url: '/api/trpc', }), ], }; } const url = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}/api/trpc` : 'http://localhost:3000/api/trpc'; return { links: [ httpBatchLink({ url, }), ], }; }, ssr: true, ssrPrepass, responseMeta(opts) { const { clientErrors } = opts; if (clientErrors.length) { // propagate http first error from API calls return { status: clientErrors[0].data?.httpStatus ?? 500, }; } // cache request for 1 day + revalidate once every second const ONE_DAY_IN_SECONDS = 60 * 60 * 24; return { headers: new Headers([ [ 'cache-control', `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`, ], ]), }; }, }); ``` ### API Response Caching with Next.js Adapter You can also use `responseMeta` on the Next.js API handler to cache API responses directly: ```tsx twoslash title='pages/api/trpc/[trpc].ts' import { initTRPC } from '@trpc/server'; import * as trpcNext from '@trpc/server/adapters/next'; export const createContext = async ({ req, res, }: trpcNext.CreateNextContextOptions) => { return { req, res, }; }; type Context = Awaited>; export const t = initTRPC.context().create(); export const appRouter = t.router({ public: t.router({ slowQueryCached: t.procedure.query(async (opts) => { await new Promise((resolve) => setTimeout(resolve, 5000)); return { lastUpdated: new Date().toJSON(), }; }), }), }); export type AppRouter = typeof appRouter; export default trpcNext.createNextApiHandler({ router: appRouter, createContext, responseMeta(opts) { const { ctx, paths, errors, type } = opts; // assuming you have all your public routes with the keyword `public` in them const allPublic = paths && paths.every((path) => path.includes('public')); // checking that no procedures errored const allOk = errors.length === 0; // checking we're doing a query request const isQuery = type === 'query'; if (ctx?.res && allPublic && allOk && isQuery) { // cache request for 1 day + revalidate once every second const ONE_DAY_IN_SECONDS = 60 * 60 * 24; return { headers: new Headers([ [ 'cache-control', `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`, ], ]), }; } return {}; }, }); ``` ## FAQ ### Q: Why do I need to forward the client's headers to the server manually? Why doesn't tRPC automatically do that for me? While it's rare that you wouldn't want to forward the client's headers to the server when doing SSR, you might want to add things dynamically in the headers. Therefore, tRPC doesn't want to take responsibility for header keys colliding, etc. ### Q: Why do I need to delete the `connection` header when using SSR on Node 18? If you don't remove the `connection` header, the data fetching will fail with `TRPCClientError: fetch failed` because `connection` is a [forbidden header name](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name). ### Q: Why do I still see network requests being made in the Network tab? By default, `@tanstack/react-query` (which we use for the data fetching hooks) refetches data on mount and window refocus, even if it's already got initial data via SSR. This ensures data is always up-to-date. See the page on [SSG](ssg) if you'd like to disable this behavior. -------------------------------------------------- --- id: starter-projects title: Starter Projects sidebar_label: Starter Projects slug: /client/nextjs/starter-projects --- Get started quickly with one of the example projects! Copy the snippet from _Quick start with `create-next-app`_ in the below list to clone the project. ## App Router
Description Links
Next.js App Router with SSE-based subscriptions and chat.
Uses @trpc/react-query with the fetch adapter.

Quick start with create-next-app npx create-next-app --example https://github.com/trpc/trpc/tree/main/examples/next-sse-chat trpc-sse-chat
## Pages Router
Description URL Links
Next.js starter with Prisma, E2E testing, & ESLint.

Quick start with create-next-app yarn create next-app --example https://github.com/trpc/trpc --example-path examples/next-prisma-starter trpc-prisma-starter
nextjs.trpc.io
zART-stack example (zero-API, TypeScript, React).
Monorepo setup with React Native, Next.js, & Prisma

Quick start with git clone git clone git@github.com:KATT/zart.git
n/a
Next.js TodoMVC-example with SSG & Prisma.

Quick start with create-next-app yarn create next-app --example https://github.com/trpc/trpc --example-path examples/next-prisma-todomvc trpc-todo
todomvc.trpc.io
-------------------------------------------------- --- id: openapi title: OpenAPI (alpha) sidebar_label: OpenAPI (alpha) slug: /openapi --- :::caution This package is in alpha. APIs may change without notice. ::: The `@trpc/openapi` package generates an OpenAPI 3.1 specification from your tRPC router. Use the spec to: - Generate a typed API client in any language - Call tRPC endpoints via HTTP tools like Postman or Insomnia - Enable AI agent integrations such as MCP servers ## Install ```bash pnpm add @trpc/openapi ``` :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: :::note `@trpc/openapi` is currently versioned like 11.x.x-alpha, and should work with any recent tRPC v11 version, but as always we recommend aligning the version numbers ::: ## Adapting your tRPC setup The generator works with your existing router — no annotations or decorators required. A few things to be aware of: - **No output types needed** — unlike other OpenAPI tools, `.output()` schemas are optional. The generator infers return types from your implementation automatically. - **Transformers** — if your server uses a [data transformer](/docs/server/data-transformers), your OpenAPI clients must use the same one. See [Transformers](#transformers) for setup and cross-language options. - **Subscriptions** — currently excluded from the generated spec. SSE support is planned. - **Descriptions** — Zod `.describe()` calls and JSDoc comments on types, routers, and procedures, all become `description` fields in the spec. ## Generate the spec ### CLI ```bash pnpm exec trpc-openapi ./src/server/router.ts ``` | Option | Default | Description | | --------------------- | -------------- | -------------------------------- | | `-e, --export ` | `AppRouter` | Name of the exported router type | | `-o, --output ` | `openapi.json` | Output file path | | `--title ` | `tRPC API` | OpenAPI `info.title` | | `--version ` | `0.0.0` | OpenAPI `info.version` | ```bash pnpm exec trpc-openapi ./src/server/router.ts -o api.json --title "My API" --version 1.0.0 ``` ### Programmatic ```ts title='scripts/generate-openapi.ts' import { generateOpenAPIDocument } from '@trpc/openapi'; const doc = generateOpenAPIDocument('./src/server/router.ts', { exportName: 'AppRouter', title: 'My API', version: '1.0.0', }); ``` The generator statically analyses your router's TypeScript types — it never executes your code. ## Generate a client from the spec Any OpenAPI client generator should work, but the most tested integration is with [Hey API](https://heyapi.dev/openapi-ts/get-started). A generated client will produce typed SDK functions matching your tRPC procedures: - **Queries** → `GET /procedure.path` - **Mutations** → `POST /procedure.path` - **Subscriptions** are ignored (SSE coming soon) ### Hey API (TypeScript) [Hey API Documentation](https://heyapi.dev/) ```bash pnpm add @trpc/openapi @hey-api/openapi-ts ``` Out of the box, an OpenAPI-generated client won't know about your transformer setup or how to encode query parameters. The `@trpc/openapi/heyapi` package provides a `configureTRPCHeyApiClient` helper that bridges this gap — it configures request serialisation, response parsing, and error deserialization so the generated SDK works correctly with tRPC endpoints. #### Without a transformer You can generate your client using Hey API's CLI or programmatic API in this case ```bash pnpm exec openapi-ts -i openapi.json -o ./generated ``` Next a little configuration is required at runtime: ```ts title='src/usage.ts' import { configureTRPCHeyApiClient } from '@trpc/openapi/heyapi'; import { client } from './generated/client.gen'; import { Sdk } from './generated/sdk.gen'; configureTRPCHeyApiClient(client, { baseUrl: 'http://localhost:3000', }); const sdk = new Sdk({ client }); const result = await sdk.greeting({ query: { input: { name: 'World' } } }); const user = await sdk.user.create({ body: { name: 'Bob', age: 30 } }); ``` #### With a transformer (superjson, devalue, etc.) :::warning If your backend uses a [data transformer](/docs/server/data-transformers) like `superjson`, you **must** pass it to the client config. Without this, Dates, Maps, Sets, and other non-JSON types may be silently wrong. ::: First generate your client code using Hey API's programmatic API, this way you can use `createTRPCHeyApiTypeResolvers` to ensure your emitted types are correct: ```ts title='src/client.ts' import { createClient } from '@hey-api/openapi-ts'; import { createTRPCHeyApiTypeResolvers } from '@trpc/openapi/heyapi'; const openApiJson = './path/to/openapi.json' const outputDir = './generated' await createClient({ input: openApiJson, output: outputDir, plugins: [ { name: '@hey-api/typescript', // Important: this ensures that your emitted types like Dates are correct '~resolvers': createTRPCHeyApiTypeResolvers(), }, { name: '@hey-api/sdk', operations: { strategy: 'single' }, }, ], }); ``` At runtime configure the generated client with your transformer, you can then pass native types directly and get them back deserialised: ```ts title='src/usage.ts' import { configureTRPCHeyApiClient } from '@trpc/openapi/heyapi'; import superjson from 'superjson'; import { client } from './generated/client.gen'; configureTRPCHeyApiClient(client, { baseUrl: 'http://localhost:3000', // Important, this transformer must match your tRPC API's transformer: transformer: superjson, }); const sdk = new Sdk({ client }); const event = await sdk.getEvent({ query: { input: { id: 'evt_1', at: new Date('2025-06-15T10:00:00Z') } }, }); // event.data.result.data.at is a Date object ✅ const created = await sdk.createEvent({ body: { name: 'Conference', at: new Date('2025-09-01T09:00:00Z') }, }); ``` ### Using a different generator or language The generated OpenAPI spec works with any OpenAPI-compatible client generator which can: - Emit accurate types for classes like Date - Support customising Search Params and request/response body serialization To integrate correctly with tRPC's protocol, you need to set up your generated client to do two things: - **Transformers** — If your tRPC API uses a transformer, the client must serialise inputs and deserialise outputs using the same format - **Query Inputs** — GET requests encode input as `?input=`, not as individual query parameters See the [Hey API config source](https://github.com/trpc/trpc/blob/f346e9bb97ff3c8a7e874f59110a47730293097a/packages/openapi/src/heyapi/index.ts) for a complete reference implementation. ### Transformers tRPC [data transformers](/docs/server/data-transformers) let you send rich types like `Date`, `Map`, `Set`, and `BigInt` over the wire. When using the OpenAPI client, the same transformer must be configured on both the server and client so that inputs are serialised and outputs are deserialised correctly. Any transformer that implements the tRPC `DataTransformer` interface (`serialize` / `deserialize`) works with `configureTRPCHeyApiClient`. Below are some tested options. #### SuperJSON The most popular transformer for TypeScript-to-TypeScript setups. Handles `Date`, `Map`, `Set`, `BigInt`, `RegExp`, and more. ```bash pnpm add superjson ``` ```ts title='src/server.ts' import { initTRPC } from '@trpc/server'; import superjson from 'superjson'; const t = initTRPC.create({ transformer: superjson }); ``` ```ts title='src/client.ts' import { configureTRPCHeyApiClient } from '@trpc/openapi/heyapi'; import superjson from 'superjson'; import { client } from './generated/client.gen'; configureTRPCHeyApiClient(client, { baseUrl: 'http://localhost:3000', transformer: superjson, }); ``` See the [superjson test](https://github.com/trpc/trpc/blob/main/packages/openapi/test/generate.test.ts) for a full end-to-end example. #### MongoDB Extended JSON v2 [EJSON](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/#mongodb-extended-json-v2-usage) is a good choice when you need cross-language support. The `bson` npm package provides `EJSON.serialize` / `EJSON.deserialize` which map directly to a tRPC `DataTransformer`. Available in: C, C#, C++, Go, Java, Node.js, Perl, PHP, Python, Ruby, Scala ```bash pnpm add bson ``` ```ts title='src/transformer.ts' import { EJSON } from 'bson'; import type { TRPCDataTransformer } from '@trpc/server'; export const ejsonTransformer: TRPCDataTransformer = { serialize: (value) => EJSON.serialize(value), deserialize: (value) => EJSON.deserialize(value as Document), }; ``` ```ts title='src/client.ts' import { configureTRPCHeyApiClient } from '@trpc/openapi/heyapi'; import { client } from './generated/client.gen'; import { ejsonTransformer } from './transformer'; configureTRPCHeyApiClient(client, { baseUrl: 'http://localhost:3000', transformer: ejsonTransformer, }); ``` See the [MongoDB EJSON test](https://github.com/trpc/trpc/blob/main/packages/openapi/test/mongoEjson.test.ts) for a full end-to-end example. #### Amazon Ion [Amazon Ion](https://amazon-ion.github.io/ion-docs/) is a richly-typed data format with broad language support. It doesn't directly support the `TRPCDataTransformer` interface and requires a bit of boilerplate to make work with tRPC in JS/TS, but may be a good choice for your own system. Available in: C, C#, D, Go, Java, JavaScript, PHP, Python, Rust ```bash pnpm add ion-js ``` See the [Amazon Ion test](https://github.com/trpc/trpc/blob/main/packages/openapi/test/amazonIon.test.ts) for the transformer implementation, boilerplate, and a full end-to-end example. #### Writing a custom transformer Any object with `serialize` and `deserialize` methods works: ```ts import type { TRPCDataTransformer } from '@trpc/server'; const myTransformer: TRPCDataTransformer = { serialize: (value) => { /* encode rich types */ }, deserialize: (value) => { /* decode them back */ }, }; ``` Pass it to both `initTRPC.create({ transformer })` on the server and `configureTRPCHeyApiClient(client, { transformer })` on the client. See the [data transformers docs](/docs/server/data-transformers) for more details. ### Full example For a complete, runnable project that ties all of these steps together, see the [openapi-codegen example](https://github.com/trpc/trpc/tree/main/examples/openapi-codegen). ## API changelog and breaking-change checks with `oasdiff` After generating OpenAPI specs, you can compare two versions with [`oasdiff`](https://github.com/oasdiff/oasdiff). For installation options (Homebrew, Docker, binaries, etc), see the official docs: - [oasdiff install docs](https://github.com/oasdiff/oasdiff/blob/main/docs/README.md) - [oasdiff changelog and breaking checks](https://github.com/oasdiff/oasdiff/blob/main/docs/BREAKING-CHANGES.md) Using this you can quick generate changelogs or detect inadvertent breaking changes to APIs to help plan and coordinate releases ```sh # Get a complete changelog including minor and breaking changes $ oasdiff changelog packages/openapi/test/routers/superjsonRouter.openapi.json /tmp/superjsonRouter.openapi.next.json 3 changes: 2 error, 0 warning, 1 info error [new-required-request-property] in API POST /createEvent added the new required request property 'location' error [api-path-removed-without-deprecation] in API GET /getBigInt api path removed without deprecation info [endpoint-added] in API GET /health endpoint added # Get a list of breaking changes if any $ oasdiff breaking packages/openapi/test/routers/superjsonRouter.openapi.json /tmp/superjsonRouter.openapi.next.json 2 changes: 2 error, 0 warning, 0 info error [new-required-request-property] in API POST /createEvent added the new required request property 'location' error [api-path-removed-without-deprecation] in API GET /getBigInt api path removed without deprecation ``` -------------------------------------------------- --- id: overview title: Client Overview sidebar_label: Overview slug: /client --- While a tRPC API can be called using normal HTTP requests like any other REST API, you will need a **client** to benefit from tRPC's typesafety. A client knows the procedures that are available in your API, and their inputs and outputs. It uses this information to give you autocomplete on your queries and mutations, correctly type the returned data, and show errors if you are writing requests that don't match the shape of your backend. If you are using React, the recommended way to call a tRPC API is by using our [TanStack React Query Integration](./tanstack-react-query/setup.mdx), which in addition to typesafe API calls also offers caching, invalidation, and management of loading and error state. If you are using Next.js with the `/pages` directory, you can use our [Next.js integration](./nextjs/overview.mdx), which adds helpers for Server-side Rendering and Static Generation. If you want to call a tRPC API from another server or from a frontend framework for which we don't have an integration, you can use the [Vanilla Client](./vanilla/overview.md). In addition to the React and Next.js integrations and the Vanilla Client, there are a variety of [community-built integrations for other frameworks](/docs/community/awesome-trpc#frontend-frameworks). Please note that these are not maintained by the tRPC team. -------------------------------------------------- --- id: aborting-procedure-calls title: Aborting Procedure Calls sidebar_label: Aborting Procedure Calls slug: /client/react/aborting-procedure-calls --- By default, tRPC does not cancel requests via React Query. If you want to opt into this behavior, you can provide `abortOnUnmount` in your configuration. :::note @tanstack/react-query only supports aborting queries. ::: ```twoslash include router import { initTRPC } from '@trpc/server'; import { z } from "zod"; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ byId: t.procedure .input(z.object({ id: z.string() })) .query(async ({input}) => { return { id: input.id, title: 'Hello' }; }), }) }); export type AppRouter = typeof appRouter; ``` ### Globally ```ts twoslash title="client.ts" // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ byId: t.procedure.input(z.object({ id: z.string() })).query(async ({ input }) => { return { id: input.id, title: 'Hello' }; }), }), }); export type AppRouter = typeof appRouter; // @filename: utils.ts // ---cut--- import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from './server'; export const trpc = createTRPCReact({ abortOnUnmount: true, }); ``` ### Per-request You may also override this behavior at the query level. ```tsx twoslash title="pages/post/[id].tsx" // @filename: server/router.ts // @include: router // @filename: utils/trpc.ts import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/router'; export const trpc = createTRPCReact(); // @filename: pages/posts.tsx declare const useRouter: any; // ---cut--- import { trpc } from '../utils/trpc'; function PostViewPage() { const { query } = useRouter(); const postQuery = trpc.post.byId.useQuery( { id: query.id }, { trpc: { abortOnUnmount: true } } ); // ... } ``` -------------------------------------------------- --- id: createTRPCQueryUtils title: createTRPCQueryUtils sidebar_label: createTRPCQueryUtils() slug: /client/react/createTRPCQueryUtils --- The use case for `createTRPCQueryUtils` is when you need to use the helpers outside of a React Component, for example in `react-router`'s loaders. Similar to `useUtils`, `createTRPCQueryUtils` is a function that gives you access to helpers that let you manage the cached data of the queries you execute via `@trpc/react-query`. These helpers are actually thin wrappers around `@tanstack/react-query`'s [`queryClient`](https://tanstack.com/query/v5/docs/reference/QueryClient) methods. If you want more in-depth information about options and usage patterns for `useUtils` helpers than what we provide here, we will link to their respective `@tanstack/react-query` docs so you can refer to them accordingly. The difference between `useUtils` and `createTRPCQueryUtils` is that `useUtils` is a react hook that uses `useQueryClient` under the hood. This means that it is able to work better within React Components. If you need access to the client directly, you can use the `client` object that you passed to `createTRPCQueryUtils` during creation. :::caution You should avoid using `createTRPCQueryUtils` in React Components. Instead, use `useUtils` which is a React hook that implements `useCallback` and `useQueryClient` under the hood. ::: ## Usage `createTRPCQueryUtils` returns an object with all the available queries you have in your routers. You use it the same way as your `trpc` client object. Once you reach a query, you'll have access to the query helpers. For example, let's say you have a `post` router with an `all` query: ```twoslash include server // @target: esnext // @filename: server.ts // ---cut--- import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ all: t.procedure.query(() => { return { posts: [ { id: 1, title: 'everlong' }, { id: 2, title: 'After Dark' }, ], }; }), }), }); export type AppRouter = typeof appRouter; ``` Now in our route loader, when we navigate the object `createTRPCQueryUtils` gives us and reach the `post.all` query, we'll get access to our query helpers! ```tsx twoslash title="MyPage.tsx" // @include: server // @filename: MyPage.tsx declare function useLoaderData(): unknown; import React from 'react'; // ---cut--- import { QueryClient } from '@tanstack/react-query'; import { createTRPCQueryUtils, createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from './server'; const trpc = createTRPCReact(); const trpcClient = trpc.createClient({ links: [] }); const queryClient = new QueryClient(); const clientUtils = createTRPCQueryUtils({ queryClient, client: trpcClient }); // This is a react-router loader export async function loader() { const allPostsData = await clientUtils.post.all.ensureData(); // Fetches data if it doesn't exist in the cache return { allPostsData, }; } // This is a react component export function Component() { const loaderData = useLoaderData() as Awaited>; const allPostQuery = trpc.post.all.useQuery(undefined, { initialData: loaderData.allPostsData, // Uses the data from the loader }); return (
{allPostQuery.data.posts.map((post) => (
{post.title}
))}
); } ``` :::note If you were using Remix Run or SSR you wouldn't re-use the same `queryClient` for every request. Instead, you would create a new `queryClient` for every request so that there's no cross-request data leakage. ::: ## Helpers Much like `useUtils`, `createTRPCQueryUtils` gives you access to same set of helpers, including `queryOptions` and `infiniteQueryOptions`. The only difference is that you need to pass in the `queryClient` and `client` objects. You can see them on the [useUtils](./useUtils.mdx#helpers)-page. -------------------------------------------------- --- id: disabling-queries title: Disabling Queries sidebar_label: Disabling Queries slug: /client/react/disabling-queries --- To disable queries, you can pass `skipToken` as the first argument to `useQuery`, `useInfiniteQuery`, and `useSubscription`. This will prevent the query from being executed. ### Typesafe conditional queries using `skipToken` ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ getUserByName: t.procedure .input(z.object({ name: z.string() })) .query(({ input }) => { return { name: input.name, email: 'user@example.com' }; }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: component.tsx // ---cut--- import React, { useState } from 'react'; import { skipToken } from '@tanstack/react-query'; import { trpc } from './utils/trpc'; export function MyComponent() { const [name, setName] = useState(); const result = trpc.getUserByName.useQuery(name ? { name: name } : skipToken); return (
{result.data?.name}
); } ``` -------------------------------------------------- --- id: getQueryKey title: getQueryKey sidebar_label: getQueryKey() slug: /client/react/getQueryKey --- We provide a getQueryKey helper that accepts a `router` or `procedure` so that you can easily provide the native function the correct query key. ```tsx twoslash type AnyQueryProcedure = any; type AnyRouter = any; type DeepPartial = T; type TInput = unknown; type TRPCQueryKey = any[]; // ---cut--- // Queries declare function getQueryKey( procedure: AnyQueryProcedure, input?: DeepPartial, type?: QueryType, /** @default 'any' */ ): TRPCQueryKey; // Routers declare function getQueryKey( router: AnyRouter, ): TRPCQueryKey; type QueryType = "query" | "infinite" | "any"; // for useQuery ──┘ │ │ // for useInfiniteQuery ────┘ │ // will match all ───────────────────────┘ ``` :::note The query type `any` will match all queries in the cache only if the `react query` method where it's used uses fuzzy matching. See [TanStack/query#5111 (comment)](https://github.com/TanStack/query/issues/5111#issuecomment-1464864361) for more context. ::: ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ list: t.procedure.query(() => { return [ { id: '1', title: 'everlong' }, { id: '2', title: 'After Dark' }, ]; }), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: component.tsx import React from 'react'; // ---cut--- import { useIsFetching, useQueryClient } from '@tanstack/react-query'; import { getQueryKey } from '@trpc/react-query'; import { trpc } from './utils/trpc'; function MyComponent() { const queryClient = useQueryClient(); const posts = trpc.post.list.useQuery(); // See if a query is fetching const postListKey = getQueryKey(trpc.post.list, undefined, 'query'); const isFetching = useIsFetching({ queryKey: postListKey }); // Set some query defaults for an entire router const postKey = getQueryKey(trpc.post); queryClient.setQueryDefaults(postKey, { staleTime: 30 * 60 * 1000 }); // ... } ``` ## Mutations Similarly to queries, we provide a getMutationKey for mutations. The underlying function is the same as getQueryKey (in fact, you could technically use getQueryKey for mutations as well), the only difference is in semantics. ```tsx import { getMutationKey } from '@trpc/react-query'; const mutationKey = getMutationKey(trpc.user.create); ``` -------------------------------------------------- --- id: infer-types title: Inferring Types sidebar_label: Inferring Types slug: /client/react/infer-types --- ```twoslash include server // @module: esnext // @filename: server.ts // ---cut--- import { initTRPC } from '@trpc/server'; import { z } from "zod"; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ list: t.procedure .query(() => { // imaginary db call return [{ id: 1, title: 'tRPC is the best!' }]; }), byId: t.procedure .input(z.string()) .query(({ input }) => { // imaginary db call return { id: 1, title: 'tRPC is the best!' }; }), create: t.procedure .input(z.object({ title: z.string(), text: z.string(), })) .mutation(({ input }) => { // imaginary db call return { id: 1, ...input }; }), }), }); export type AppRouter = typeof appRouter; ``` In addition to the type inference made available by `@trpc/server` ([see here](/docs/client/vanilla/infer-types)) this integration also provides some inference helpers for usage purely in React. ## Infer React Query options based on your router When creating custom hooks around tRPC procedures, it's sometimes necessary to have the types of the options inferred from the router. You can do so via the `inferReactQueryProcedureOptions` helper exported from `@trpc/react-query`. ```ts twoslash title='trpc.ts' // @module: esnext // @include: server // @filename: trpc.ts // ---cut--- import { createTRPCReact, type inferReactQueryProcedureOptions, } from '@trpc/react-query'; import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; import type { AppRouter } from './server'; // infer the types for your router export type ReactQueryOptions = inferReactQueryProcedureOptions; export type RouterInputs = inferRouterInputs; export type RouterOutputs = inferRouterOutputs; export const trpc = createTRPCReact(); ``` ```ts twoslash title='usePostCreate.ts' // @module: esnext // @include: server // @filename: trpc.ts import { createTRPCReact, type inferReactQueryProcedureOptions, } from '@trpc/react-query'; import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; import type { AppRouter } from './server'; export type ReactQueryOptions = inferReactQueryProcedureOptions; export type RouterInputs = inferRouterInputs; export type RouterOutputs = inferRouterOutputs; export const trpc = createTRPCReact(); // @filename: usePostCreate.ts // ---cut--- import { trpc, type ReactQueryOptions, type RouterInputs, type RouterOutputs, } from './trpc'; type PostCreateOptions = ReactQueryOptions['post']['create']; function usePostCreate(options?: PostCreateOptions) { const utils = trpc.useUtils(); return trpc.post.create.useMutation({ ...options, onSuccess(post, variables, onMutateResult, context) { // invalidate all queries on the post router // when a new post is created utils.post.invalidate(); options?.onSuccess?.(post, variables, onMutateResult, context); }, }); } ``` ```ts twoslash title='usePostById.ts' // @module: esnext // @include: server // @filename: trpc.ts import { createTRPCReact, type inferReactQueryProcedureOptions, } from '@trpc/react-query'; import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; import type { AppRouter } from './server'; export type ReactQueryOptions = inferReactQueryProcedureOptions; export type RouterInputs = inferRouterInputs; export type RouterOutputs = inferRouterOutputs; export const trpc = createTRPCReact(); // @filename: usePostById.ts // ---cut--- import { ReactQueryOptions, RouterInputs, trpc } from './trpc'; type PostByIdOptions = ReactQueryOptions['post']['byId']; type PostByIdInput = RouterInputs['post']['byId']; function usePostById(input: PostByIdInput, options?: PostByIdOptions) { return trpc.post.byId.useQuery(input, options); } ``` ## Infer abstract types from a "Router Factory" If you write a factory which creates a similar router interface several times in your application, you may wish to share client code between usages of the factory. `@trpc/react-query/shared` exports several types which can be used to generate abstract types for a router factory, and build common React components which are passed the router as a prop. ```tsx twoslash title='api/factory.ts' // @module: esnext // @filename: trpc.ts import { initTRPC } from '@trpc/server'; const t2 = initTRPC.create(); export const t = t2; export const publicProcedure = t2.procedure; // @filename: factory.ts // ---cut--- import { z } from 'zod'; import { t, publicProcedure } from './trpc'; // @trpc/react-query/shared exports several **Like types which can be used to generate abstract types import { RouterLike, UtilsLike } from '@trpc/react-query/shared'; const ThingRequest = z.object({ name: z.string() }); const Thing = z.object({ id: z.string(), name: z.string() }); const ThingQuery = z.object({ filter: z.string().optional() }); const ThingArray = z.array(Thing); // Factory function written by you, however you need, // so long as you can infer the resulting type of t.router() later export function createMyRouter() { return t.router({ createThing: publicProcedure .input(ThingRequest) .output(Thing) .mutation(({ input }) => ({ id: '1', ...input })), listThings: publicProcedure .input(ThingQuery) .output(ThingArray) .query(() => []), }) } // Infer the type of your router, and then generate the abstract types for use in the client type MyRouterType = ReturnType export type MyRouterLike = RouterLike export type MyRouterUtilsLike = UtilsLike ``` ```tsx twoslash title='api/server.ts' // @module: esnext // @filename: trpc.ts import { initTRPC } from '@trpc/server'; const t2 = initTRPC.create(); export const t = t2; export const publicProcedure = t2.procedure; // @filename: factory.ts import { z } from 'zod'; import { t, publicProcedure } from './trpc'; import { RouterLike, UtilsLike } from '@trpc/react-query/shared'; const ThingRequest = z.object({ name: z.string() }); const Thing = z.object({ id: z.string(), name: z.string() }); const ThingQuery = z.object({ filter: z.string().optional() }); const ThingArray = z.array(Thing); export function createMyRouter() { return t.router({ createThing: publicProcedure.input(ThingRequest).output(Thing).mutation(({ input }) => ({ id: '1', ...input })), listThings: publicProcedure.input(ThingQuery).output(ThingArray).query(() => []), }) } type MyRouterType = ReturnType export type MyRouterLike = RouterLike export type MyRouterUtilsLike = UtilsLike // @filename: app-server.ts import { t } from './trpc'; import { createMyRouter } from './factory'; const appRouter = t.router({ things: createMyRouter() }); // ---cut--- export type AppRouter = typeof appRouter; // Export your MyRouter types to the client export type { MyRouterLike, MyRouterUtilsLike } from './factory'; ``` ```tsx twoslash title='frontend/usePostCreate.ts' // @module: esnext // @jsx: react-jsx // @filename: trpc.ts import { initTRPC } from '@trpc/server'; const t2 = initTRPC.create(); export const t = t2; export const publicProcedure = t2.procedure; // @filename: factory.ts import { z } from 'zod'; import { t, publicProcedure } from './trpc'; import { RouterLike, UtilsLike } from '@trpc/react-query/shared'; const ThingRequest = z.object({ name: z.string() }); const Thing = z.object({ id: z.string(), name: z.string() }); const ThingQuery = z.object({ filter: z.string().optional() }); const ThingArray = z.array(Thing); export function createMyRouter() { return t.router({ createThing: publicProcedure.input(ThingRequest).output(Thing).mutation(({ input }) => ({ id: '1', ...input })), listThings: publicProcedure.input(ThingQuery).output(ThingArray).query(() => []), doThing: publicProcedure.input(z.object({ name: z.string() })).mutation(({ input }) => input), }) } type MyRouterType = ReturnType export type MyRouterLike = RouterLike export type MyRouterUtilsLike = UtilsLike // @filename: app-router.ts import { t } from './trpc'; import { createMyRouter } from './factory'; const appRouter = t.router({ deep: t.router({ route: t.router({ things: createMyRouter(), }), }), different: t.router({ things: createMyRouter(), }), }); export type AppRouter = typeof appRouter; // @filename: trpc-client.ts import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from './app-router'; export const trpc = createTRPCReact(); // @filename: use-post-create.tsx import { trpc } from './trpc-client'; const useUtils = trpc.useUtils; // ---cut--- import type { MyRouterLike, MyRouterUtilsLike } from './factory'; type MyGenericComponentProps = { route: MyRouterLike; utils: MyRouterUtilsLike; }; function MyGenericComponent(props: MyGenericComponentProps) { const { route } = props; const thing = route.listThings.useQuery({ filter: 'qwerty', }); const mutation = route.doThing.useMutation({ onSuccess() { props.utils.listThings.invalidate(); }, }); function handleClick() { mutation.mutate({ name: 'Thing 1', }); } return null; /* ui */ } function MyPageComponent() { const utils = useUtils(); return ( ); } function MyOtherPageComponent() { const utils = useUtils(); return ( ); } ``` A more complete working example [can be found here](https://github.com/trpc/trpc/tree/main/packages/tests/server/react/polymorphism.test.tsx) -------------------------------------------------- --- id: overview title: React Query Integration (Classic) sidebar_label: Overview description: React Query Integration slug: /client/react --- :::tip These are the docs for our 'Classic' React Query integration, which (while still supported) is not the recommended way to start new tRPC projects with TanStack React Query. We recommend using the new [TanStack React Query Integration](/docs/client/tanstack-react-query/setup) instead. ::: tRPC offers a first class integration with React. Under the hood this is simply a wrapper around the very popular [@tanstack/react-query](https://tanstack.com/query/latest), so we recommend that you familiarize yourself with React Query, as their docs go into much greater depth on its usage. If you are using Next.js we recommend using [our integration with that](../nextjs/overview.mdx) instead.
❓ Do I have to use an integration? No! The integration is fully optional. You can use `@tanstack/react-query` using just a [vanilla tRPC client](/docs/client/vanilla), although then you'll have to manually manage query keys and do not get the same level of DX as when using the integration package. ```ts twoslash title='utils/trpc.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ list: t.procedure.query(() => { return [{ id: 1, title: 'everlong' }]; }), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../server'; export const trpc = createTRPCClient({ links: [httpBatchLink({ url: 'YOUR_API_URL' })], }); ``` ```tsx twoslash title='components/PostList.tsx' // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ list: t.procedure.query(() => { return [{ id: 1, title: 'everlong' }]; }), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../server'; export const trpc = createTRPCClient({ links: [httpBatchLink({ url: 'YOUR_API_URL' })], }); // @filename: components/PostList.tsx // ---cut--- import { useQuery } from '@tanstack/react-query'; import { trpc } from '../utils/trpc'; function PostList() { const { data } = useQuery({ queryKey: ['posts'], queryFn: () => trpc.post.list.query(), }); data; // Post[] // ... } ```
### The tRPC React Query Integration This library enables usage directly within React components. ```tsx twoslash title='pages/IndexPage.tsx' // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ hello: t.procedure .input(z.object({ name: z.string() })) .query(({ input }) => ({ greeting: `Hello ${input.name}` })), goodbye: t.procedure.mutation(() => 'goodbye'), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: pages/IndexPage.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; export default function IndexPage() { const helloQuery = trpc.hello.useQuery({ name: 'Bob' }); const goodbyeMutation = trpc.goodbye.useMutation(); return (

{helloQuery.data?.greeting}

); } ``` ### Differences to vanilla React Query The wrapper abstracts some aspects of React Query for you: - Query Keys - these are generated and managed by tRPC on your behalf, based on the procedure inputs you provide - If you need the query key which tRPC calculates, you can use [getQueryKey](/docs/client/react/getQueryKey) - Type safe by default - the types you provide in your tRPC Backend also drive the types of your React Query client, providing safety throughout your React app -------------------------------------------------- --- id: server-components title: Set up with React Server Components sidebar_label: Server Components slug: /client/react/server-components --- import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs'; :::tip These are the docs for our 'Classic' React Query integration, which (while still supported) is not the recommended way to start new tRPC projects with TanStack React Query. We recommend using the new [TanStack React Query Integration](/docs/client/tanstack-react-query/server-components) instead. ::: :::tip **Using Next.js?** See the dedicated [Next.js App Router setup guide](/docs/client/nextjs/app-router-setup) for the recommended approach. ::: This guide is an overview of how one may use tRPC with a React Server Components (RSC) framework such as Next.js App Router. Be aware that RSC on its own solves a lot of the same problems tRPC was designed to solve, so you may not need tRPC at all. There is also not a one-size-fits-all way to integrate tRPC with RSCs, so see this guide as a starting point and adjust it to your needs and preferences. :::info If you're looking for how to use tRPC with Server Actions, see the [Server Actions guide](/docs/client/nextjs/server-actions). ::: :::caution Please read React Query's [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr) docs before proceeding to understand the different types of server rendering and what footguns to avoid. ::: ## Add tRPC to existing projects ### 1. Install deps import { InstallSnippet } from '@site/src/components/InstallSnippet'; ### 2. Create a tRPC router Initialize your tRPC backend in `trpc/init.ts` using the `initTRPC` function, and create your first router. We're going to make a simple "hello world" router and procedure here - but for deeper information on creating your tRPC API you should refer to the [Quickstart guide](/docs/quickstart) and [Backend usage docs](/docs/server/overview) for tRPC information. :::info The file names used here are not enforced by tRPC. You may use any file structure you wish. :::
View sample backend ```ts twoslash title='trpc/init.ts' import { initTRPC } from '@trpc/server'; import { cache } from 'react'; export const createTRPCContext = cache(async () => { /** * @see: https://trpc.io/docs/server/context */ return { userId: 'user_123' }; }); // Avoid exporting the entire t-object // since it's not very descriptive. // For instance, the use of a t variable // is common in i18n libraries. const t = initTRPC.create({ /** * @see https://trpc.io/docs/server/data-transformers */ // transformer: superjson, }); // Base router and procedure helpers export const createTRPCRouter = t.router; export const createCallerFactory = t.createCallerFactory; export const baseProcedure = t.procedure; ```
```ts twoslash title='trpc/routers/_app.ts' // @filename: init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCRouter = t.router; export const baseProcedure = t.procedure; // @filename: routers/_app.ts // ---cut--- import { z } from 'zod'; import { baseProcedure, createTRPCRouter } from '../init'; export const appRouter = createTRPCRouter({ hello: baseProcedure .input( z.object({ text: z.string(), }), ) .query((opts) => { return { greeting: `hello ${opts.input.text}`, }; }), }); // export type definition of API export type AppRouter = typeof appRouter; ```
:::note The backend adapter depends on your framework and how it sets up API routes. The following example sets up GET and POST routes at `/api/trpc/*` using the [fetch adapter](https://trpc.io/docs/server/adapters/fetch) in Next.js. ::: ```ts twoslash title='app/api/trpc/[trpc]/route.ts' // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCRouter = t.router; export const baseProcedure = t.procedure; export const createTRPCContext = async () => { return { userId: 'user_123' }; }; // @filename: trpc/routers/_app.ts import { z } from 'zod'; import { baseProcedure, createTRPCRouter } from '../init'; export const appRouter = createTRPCRouter({ hello: baseProcedure .input(z.object({ text: z.string() })) .query((opts) => ({ greeting: `hello ${opts.input.text}` })), }); export type AppRouter = typeof appRouter; // @filename: app/api/trpc/[trpc]/route.ts // ---cut--- import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { createTRPCContext } from '../../../../trpc/init'; import { appRouter } from '../../../../trpc/routers/_app'; const handler = (req: Request) => fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext: createTRPCContext, }); export { handler as GET, handler as POST }; ```
### 3. Create a Query Client factory Create a shared file `trpc/query-client.ts` that exports a function that creates a `QueryClient` instance. ```ts twoslash title='trpc/query-client.ts' import { defaultShouldDehydrateQuery, QueryClient, } from '@tanstack/react-query'; import superjson from 'superjson'; export function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 30 * 1000, }, dehydrate: { // serializeData: superjson.serialize, shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', }, hydrate: { // deserializeData: superjson.deserialize, }, }, }); } ``` We're setting a few default options here: - `staleTime`: With SSR, we usually want to set some default staleTime above 0 to avoid refetching immediately on the client. - `shouldDehydrateQuery`: This is a function that determines whether a query should be dehydrated or not. Since the RSC transport protocol supports hydrating promises over the network, we extend the `defaultShouldDehydrateQuery` function to also include queries that are still pending. This will allow us to start prefetching in a server component high up the tree, then consuming that promise in a client component further down. - `serializeData` and `deserializeData` (optional): If you set up a [data transformer](https://trpc.io/docs/server/data-transformers) in the previous step, set this option to make sure the data is serialized correctly when hydrating the query client over the server-client boundary. ### 4. Create a tRPC client for Client Components The `trpc/client.tsx` is the entrypoint when consuming your tRPC API from client components. In here, import the **type definition** of your tRPC router and create typesafe hooks using `createTRPCReact`. We'll also export our context provider from this file. ```tsx twoslash title='trpc/client.tsx' // @filename: trpc/query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 30 * 1000 } } }); } // @filename: trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure .input(z.object({ text: z.string() })) .query((opts) => ({ greeting: `hello ${opts.input.text}` })), }); export type AppRouter = typeof appRouter; // @filename: trpc/client.tsx // ---cut--- 'use client'; // ^-- to make sure we can mount the Provider from a server component import type { QueryClient } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query'; import { httpBatchLink } from '@trpc/client'; import { createTRPCReact } from '@trpc/react-query'; import React, { useState } from 'react'; import { makeQueryClient } from './query-client'; import type { AppRouter } from './routers/_app'; export const trpc = createTRPCReact(); let clientQueryClientSingleton: QueryClient; function getQueryClient() { if (typeof window === 'undefined') { // Server: always make a new query client return makeQueryClient(); } // Browser: use singleton pattern to keep the same query client return (clientQueryClientSingleton ??= makeQueryClient()); } function getUrl() { const base = (() => { if (typeof window !== 'undefined') return ''; if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; return 'http://localhost:3000'; })(); return `${base}/api/trpc`; } export function TRPCProvider( props: Readonly<{ children: React.ReactNode; }>, ) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient(); const [trpcClient] = useState(() => trpc.createClient({ links: [ httpBatchLink({ // transformer: superjson, <-- if you use a data transformer url: getUrl(), }), ], }), ); return ( {props.children} ); } ``` Mount the provider in the root of your application (e.g. `app/layout.tsx` when using Next.js). ### 5. Create a tRPC caller for Server Components To prefetch queries from server components, we use a tRPC caller. The `@trpc/react-query/rsc` module exports a thin wrapper around [`createCaller`](https://trpc.io/docs/server/server-side-calls) that integrates with your React Query client. ```tsx twoslash title='trpc/server.tsx' // @filename: server-only.d.ts export {}; // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCRouter = t.router; export const createCallerFactory = t.createCallerFactory; export const baseProcedure = t.procedure; export const createTRPCContext = async () => ({ userId: 'user_123' }); // @filename: trpc/query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: trpc/routers/_app.ts import { z } from 'zod'; import { baseProcedure, createTRPCRouter } from '../init'; export const appRouter = createTRPCRouter({ hello: baseProcedure .input(z.object({ text: z.string() })) .query((opts) => ({ greeting: `hello ${opts.input.text}` })), }); export type AppRouter = typeof appRouter; // @filename: trpc/server.tsx // ---cut--- import 'server-only'; // <-- ensure this file cannot be imported from the client import { createHydrationHelpers } from '@trpc/react-query/rsc'; import { cache } from 'react'; import { createCallerFactory, createTRPCContext } from './init'; import { makeQueryClient } from './query-client'; import { appRouter } from './routers/_app'; // IMPORTANT: Create a stable getter for the query client that // will return the same client during the same request. export const getQueryClient = cache(makeQueryClient); const caller = createCallerFactory(appRouter)(createTRPCContext); export const { trpc, HydrateClient } = createHydrationHelpers( caller, getQueryClient, ); ``` ## Using your API Now you can use your tRPC API in your app. While you can use the React Query hooks in client components just like you would in any other React app, we can take advantage of the RSC capabilities by prefetching queries in a server component high up the tree. You may be familiar with this concept as "render as you fetch" commonly implemented as loaders. This means the request fires as soon as possible but without suspending until the data is needed by using the `useQuery` or `useSuspenseQuery` hooks. ```tsx twoslash title='app/page.tsx' // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure .input(z.object({ text: z.string() }).optional()) .query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}` })), }); export type AppRouter = typeof appRouter; export const createCallerFactory = t.createCallerFactory; // @filename: trpc/server.tsx import { createHydrationHelpers } from '@trpc/react-query/rsc'; import { QueryClient } from '@tanstack/react-query'; import { cache } from 'react'; import { createCallerFactory, appRouter } from './init'; const getQueryClient = cache(() => new QueryClient()); const caller = createCallerFactory(appRouter)(async () => ({})); export const { trpc, HydrateClient } = createHydrationHelpers(caller, getQueryClient); // @filename: app/client-greeting.tsx export function ClientGreeting() { return null as any; } // @filename: app/page.tsx import React from 'react'; // ---cut--- import { trpc, HydrateClient } from '../trpc/server'; import { ClientGreeting } from './client-greeting'; export default async function Home() { void trpc.hello.prefetch(); return (
...
{/** ... */}
); } ``` ```tsx twoslash title='app/client-greeting.tsx' // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ hello: t.procedure .input(z.object({ text: z.string() }).optional()) .query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}` })), }); export type AppRouter = typeof appRouter; // @filename: trpc/client.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: app/client-greeting.tsx import React from 'react'; // ---cut--- 'use client'; // <-- hooks can only be used in client components import { trpc } from '../trpc/client'; export function ClientGreeting() { const greeting = trpc.hello.useQuery(); if (!greeting.data) return
Loading...
; return
{greeting.data.greeting}
; } ``` ### Leveraging Suspense You may prefer handling loading and error states using Suspense and Error Boundaries. You can do this by using the `useSuspenseQuery` hook. ```tsx twoslash title='app/page.tsx' // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure .input(z.object({ text: z.string() }).optional()) .query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}` })), }); export type AppRouter = typeof appRouter; export const createCallerFactory = t.createCallerFactory; // @filename: trpc/server.tsx import { createHydrationHelpers } from '@trpc/react-query/rsc'; import { QueryClient } from '@tanstack/react-query'; import { cache } from 'react'; import { createCallerFactory, appRouter } from './init'; const getQueryClient = cache(() => new QueryClient()); const caller = createCallerFactory(appRouter)(async () => ({})); export const { trpc, HydrateClient } = createHydrationHelpers(caller, getQueryClient); // @filename: react-error-boundary.d.ts import React from 'react'; export declare function ErrorBoundary(props: { fallback: React.ReactNode; children: React.ReactNode }): React.JSX.Element; // @filename: app/client-greeting.tsx export function ClientGreeting() { return null as any; } // @filename: app/page.tsx import React from 'react'; // ---cut--- import { trpc, HydrateClient } from '../trpc/server'; import { Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { ClientGreeting } from './client-greeting'; export default async function Home() { void trpc.hello.prefetch(); return (
...
{/** ... */} Something went wrong}> Loading...}>
); } ``` ```tsx twoslash title='app/client-greeting.tsx' // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ hello: t.procedure .input(z.object({ text: z.string() }).optional()) .query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}` })), }); export type AppRouter = typeof appRouter; // @filename: trpc/client.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: app/client-greeting.tsx import React from 'react'; // ---cut--- 'use client'; import { trpc } from '../trpc/client'; export function ClientGreeting() { const [data] = trpc.hello.useSuspenseQuery(); return
{data.greeting}
; } ``` ### Getting data in a server component If you need access to the data in a server component, you can invoke the procedure directly instead of using `.prefetch()`, just like you use [the normal server caller](/docs/server/server-side-calls). Please note that this method is detached from your query client and does not store the data in the cache. This means that you cannot use the data in a server component and expect it to be available in the client. This is intentional and explained in more detail in the [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#data-ownership-and-revalidation) guide. ```tsx twoslash title='app/page.tsx' // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure .input(z.object({ text: z.string() }).optional()) .query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}` })), }); export type AppRouter = typeof appRouter; export const createCallerFactory = t.createCallerFactory; // @filename: trpc/server.tsx import { createHydrationHelpers } from '@trpc/react-query/rsc'; import { QueryClient } from '@tanstack/react-query'; import { cache } from 'react'; import { createCallerFactory, appRouter } from './init'; const getQueryClient = cache(() => new QueryClient()); const caller = createCallerFactory(appRouter)(async () => ({})); export const { trpc } = createHydrationHelpers(caller, getQueryClient); // @filename: app/page.tsx import React from 'react'; // ---cut--- import { trpc } from '../trpc/server'; export default async function Home() { // Use the caller directly without using `.prefetch()` const greeting = await trpc.hello(); // ^? { greeting: string } return
{greeting.greeting}
; } ``` -------------------------------------------------- --- id: setup title: Set up the React Query Integration sidebar_label: Setup description: How to use and setup tRPC in React slug: /client/react/setup --- import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs'; ### 1. Install dependencies The following dependencies should be installed. import { InstallSnippet } from '@site/src/components/InstallSnippet'; :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: ### 2. Import your `AppRouter` ```twoslash include router // @filename: server/router.ts // ---cut--- import { initTRPC } from '@trpc/server'; import { z } from "zod"; const t = initTRPC.create(); const appRouter = t.router({ getUser: t.procedure.input(z.object({ id: z.string() })).query(() => ({ name: 'foo' })), createUser: t.procedure.input(z.object({ name: z.string() })).mutation(() => 'bar'), }); export type AppRouter = typeof appRouter; ``` import ImportAppRouter from '../../partials/_import-approuter.mdx'; ### 3. Create tRPC hooks Create a set of strongly-typed React hooks from your `AppRouter` type signature with `createTRPCReact`. ```ts twoslash title='utils/trpc.ts' // @include: router // @filename: utils/trpc.ts // ---cut--- import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/router'; export const trpc = createTRPCReact(); ``` ### 4. Add tRPC providers Create a tRPC client, and wrap your application in the tRPC Provider, as below. You will also need to set up and connect React Query, which [they document in more depth](https://tanstack.com/query/latest/docs/framework/react/quick-start). :::tip If you already use React Query in your application, you **should** re-use the `QueryClient` and `QueryClientProvider` you already have. ::: ```tsx twoslash title='App.tsx' // @include: router // @filename: utils/trpc.ts import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/router'; export const trpc = createTRPCReact(); // @filename: App.tsx declare function getAuthCookie(): string; // ---cut--- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { httpBatchLink } from '@trpc/client'; import React, { useState } from 'react'; import { trpc } from './utils/trpc'; export function App() { const [queryClient] = useState(() => new QueryClient()); const [trpcClient] = useState(() => trpc.createClient({ links: [ httpBatchLink({ url: 'http://localhost:3000/trpc', // You can pass any HTTP headers you wish here async headers() { return { authorization: getAuthCookie(), }; }, }), ], }), ); return ( {/* Your app here */} ); } ``` :::note The reason for using `useState` in the creation of the `queryClient` and the `TRPCClient`, as opposed to declaring them outside of the component, is to ensure that each request gets a unique client when using SSR. If you use client side rendering then you can move them if you wish. ::: ### 5. Fetch data You can now use the tRPC React Query integration to call queries and mutations on your API. ```tsx twoslash title='pages/IndexPage.tsx' // @include: router // @filename: utils/trpc.ts import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/router'; export const trpc = createTRPCReact(); // @filename: pages/IndexPage.tsx import React from "react"; // ---cut--- import { trpc } from '../utils/trpc'; export default function IndexPage() { const userQuery = trpc.getUser.useQuery({ id: 'id_bilbo' }); const userCreator = trpc.createUser.useMutation(); return (

{userQuery.data?.name}

); } ``` -------------------------------------------------- --- id: suspense title: Suspense sidebar_label: Suspense slug: /client/react/suspense --- :::info - Ensure you're on the latest version of React - If you use suspense with [tRPC's _automatic_ SSR in Next.js](/docs/client/nextjs/pages-router/ssr), the full page will crash on the server if a query fails, even if you have an `` ::: ## Usage :::tip `useSuspenseQuery` & `useSuspenseInfiniteQuery` both return a `[data, query]`-_tuple_, to make it easy to directly use your data and renaming the variable to something descriptive ::: ```twoslash include server // @target: esnext // @filename: server.ts import { initTRPC, TRPCError } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const posts = [ { id: '1', title: 'everlong' }, { id: '2', title: 'After Dark' }, ]; const appRouter = t.router({ post: t.router({ all: t.procedure .input( z.object({ cursor: z.string().optional(), }) ) .query(({ input }) => { return { posts, nextCursor: '123' as string | undefined, }; }), byId: t.procedure .input( z.object({ id: z.string(), }) ) .query(({ input }) => { const post = posts.find(p => p.id === input.id); if (!post) { throw new TRPCError({ code: 'NOT_FOUND', }) } return post; }), }), }); export type AppRouter = typeof appRouter; export interface PostPage { posts: { id: string; title: string }[]; nextCursor?: string | undefined; } // @filename: utils/trpc.tsx // ---cut--- import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); ``` ### `useSuspenseQuery()` ```tsx twoslash // @target: esnext // @include: server // @filename: pages/index.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; function PostView() { const [post, postQuery] = trpc.post.byId.useSuspenseQuery({ id: '1' }); // ^? return <>{/* ... */}; } ``` ### `useSuspenseInfiniteQuery()` ```tsx twoslash // @target: esnext // @include: server // @filename: pages/index.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; import type { PostPage } from '../server'; function PostView() { const [{ pages }, allPostsQuery] = trpc.post.all.useSuspenseInfiniteQuery( {}, { getNextPageParam(lastPage: PostPage) { return lastPage.nextCursor; }, initialCursor: '', }, ); const { isFetching, isFetchingNextPage, fetchNextPage, hasNextPage } = allPostsQuery; return <>{/* ... */}; } ``` ### `useSuspenseQueries()` Suspense equivalent of [`useQueries()`](./useQueries.md). ```tsx twoslash // @target: esnext // @include: server // @filename: pages/index.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; const Component = (props: { postIds: string[] }) => { const [posts, postQueries] = trpc.useSuspenseQueries((t) => props.postIds.map((id) => t.post.byId({ id })), ); return <>{/* [...] */}; }; ``` ## Prefetching The performance of suspense queries can be improved by prefetching the query data before the Suspense component is rendered (this is sometimes called ["render-as-you-fetch"](https://tanstack.com/query/v5/docs/framework/react/guides/suspense#fetch-on-render-vs-render-as-you-fetch)). :::note - Prefetching and the render-as-you-fetch model are very dependent on the framework and router you are using. We recommend reading your frameworks router docs along with the [@tanstack/react-query docs](https://tanstack.com/query/v5/docs/react/guides/prefetching) to understand how to implement these patterns. - If you are using Next.js please look at the docs on [Server-Side Helpers](/docs/client/nextjs/pages-router/server-side-helpers) to implement server-side prefetching. ::: ### Route-level prefetching ```tsx twoslash // @target: esnext // @include: server // @filename: loader.ts // ---cut--- import { createTRPCQueryUtils } from '@trpc/react-query'; import { createTRPCClient, httpBatchLink } from '@trpc/client'; import { QueryClient } from '@tanstack/react-query'; import type { AppRouter } from './server'; const queryClient = new QueryClient(); const trpcClient = createTRPCClient({ links: [httpBatchLink({ url: 'http://localhost:3000' })] }); const utils = createTRPCQueryUtils({ queryClient, client: trpcClient }); // tanstack router/ react router loader const loader = async (params: { id: string }) => utils.post.byId.ensureData({ id: params.id }); ``` ### Component-level prefetching with `usePrefetchQuery` ```tsx twoslash // @target: esnext // @include: server // @filename: pages/index.tsx // ---cut--- import React, { Suspense } from 'react'; import { trpc } from '../utils/trpc'; function PostView(props: { postId: string }) { return <>; } function PostViewPage(props: { postId: string }) { trpc.post.byId.usePrefetchQuery({ id: props.postId }); return ( ); } ``` ### Component-level prefetching with `usePrefetchInfiniteQuery` ```tsx twoslash // @target: esnext // @include: server // @filename: pages/index.tsx // ---cut--- import React, { Suspense } from 'react'; import { trpc } from '../utils/trpc'; import type { PostPage } from '../server'; function PostView(props: { postId: string }) { return <>; } function PostViewPage(props: { postId: string }) { trpc.post.all.usePrefetchInfiniteQuery( {}, { getNextPageParam(lastPage: PostPage) { return lastPage.nextCursor; }, initialCursor: '', }, ); return ( ); } ``` -------------------------------------------------- --- id: useInfiniteQuery title: useInfiniteQuery() sidebar_label: useInfiniteQuery() slug: /client/react/useInfiniteQuery --- :::info - Your procedure needs to accept a `cursor` input of any type (`string`, `number`, etc) to expose this hook. - For more details on infinite queries read the [react-query docs](https://tanstack.com/query/v5/docs/framework/react/reference/useInfiniteQuery) - In this example we're using Prisma - see their docs on [cursor-based pagination](https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination) ::: ## Example Procedure ```tsx twoslash title='server/routers/_app.ts' // @filename: server/routers/_app.ts declare const prisma: { post: { findMany: (opts: any) => Promise<{ myCursor: number }[]>; }; }; // ---cut--- import { initTRPC } from '@trpc/server'; import { z } from 'zod'; export const t = initTRPC.create(); export const appRouter = t.router({ infinitePosts: t.procedure .input( z.object({ limit: z.number().min(1).max(100).nullish(), cursor: z.number().nullish(), // <-- "cursor" needs to exist, but can be any type direction: z.enum(['forward', 'backward']), // optional, useful for bi-directional query }), ) .query(async (opts) => { const { input } = opts; const limit = input.limit ?? 50; const { cursor } = input; const items = await prisma.post.findMany({ take: limit + 1, // get an extra item at the end which we'll use as next cursor where: { title: { contains: 'Prisma' /* Optional filter */, }, }, cursor: cursor ? { myCursor: cursor } : undefined, orderBy: { myCursor: 'asc', }, }); let nextCursor: typeof cursor | undefined = undefined; if (items.length > limit) { const nextItem = items.pop(); nextCursor = nextItem!.myCursor; } return { items, nextCursor, }; }), }); ``` ## Example React Component ```tsx twoslash title='components/MyComponent.tsx' // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ infinitePosts: t.procedure .input(z.object({ limit: z.number().min(1).max(100).nullish(), cursor: z.number().nullish(), direction: z.enum(['forward', 'backward']), })) .query(({ input }) => { return { items: [] as { id: string; title: string }[], nextCursor: 1 as number | undefined, }; }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: components/MyComponent.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; export function MyComponent() { const myQuery = trpc.infinitePosts.useInfiniteQuery( { limit: 10, }, { getNextPageParam: (lastPage) => lastPage.nextCursor, // initialCursor: 1, // <-- optional you can pass an initialCursor }, ); // [...] } ``` ## Helpers ### `getInfiniteData()` This helper gets the currently cached data from an existing infinite query ```tsx twoslash title='components/MyComponent.tsx' // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ infinitePosts: t.router({ list: t.procedure .input(z.object({ limit: z.number().min(1).max(100).nullish(), cursor: z.number().nullish(), direction: z.enum(['forward', 'backward']).optional(), })) .query(({ input }) => { return { items: [] as { id: string; title: string; status: string }[], nextCursor: 1 as number | undefined, }; }), add: t.procedure .input(z.object({ title: z.string() })) .mutation(({ input }) => { return { id: '1', title: input.title }; }), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: components/MyComponent.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; export function MyComponent() { const utils = trpc.useUtils(); const myMutation = trpc.infinitePosts.add.useMutation({ async onMutate(opts) { await utils.infinitePosts.list.cancel(); const allPosts = utils.infinitePosts.list.getInfiniteData({ limit: 10 }); // [...] }, }); } ``` ### `setInfiniteData()` This helper allows you to update a query's cached data ```tsx twoslash title='components/MyComponent.tsx' // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ infinitePosts: t.router({ list: t.procedure .input(z.object({ limit: z.number().min(1).max(100).nullish(), cursor: z.number().nullish(), direction: z.enum(['forward', 'backward']).optional(), })) .query(({ input }) => { return { items: [] as { id: string; title: string; status: string }[], nextCursor: 1 as number | undefined, }; }), delete: t.procedure .input(z.object({ id: z.string() })) .mutation(({ input }) => { return { id: input.id }; }), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: components/MyComponent.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; export function MyComponent() { const utils = trpc.useUtils(); const myMutation = trpc.infinitePosts.delete.useMutation({ async onMutate(opts) { await utils.infinitePosts.list.cancel(); utils.infinitePosts.list.setInfiniteData({ limit: 10 }, (data) => { if (!data) { return { pages: [], pageParams: [], }; } return { ...data, pages: data.pages.map((page) => ({ ...page, items: page.items.filter((item) => item.status === 'published'), })), }; }); }, }); // [...] } ``` -------------------------------------------------- --- id: useMutation title: useMutation() sidebar_label: useMutation() slug: /client/react/useMutation --- :::note The hooks provided by `@trpc/react-query` are a thin wrapper around @tanstack/react-query. For in-depth information about options and usage patterns, refer to their docs on [mutations](https://tanstack.com/query/v5/docs/framework/react/guides/mutations). ::: ### Example
Backend code ```tsx twoslash title='server/routers/_app.ts' import { initTRPC } from '@trpc/server'; import { z } from 'zod'; export const t = initTRPC.create(); export const appRouter = t.router({ // Create procedure at path 'login' // The syntax is identical to creating queries login: t.procedure // using zod schema to validate and infer input values .input( z.object({ name: z.string(), }), ) .mutation((opts) => { // Here some login stuff would happen return { user: { name: opts.input.name, role: 'ADMIN' as const, }, }; }), }); export type AppRouter = typeof appRouter; ```
```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ login: t.procedure .input(z.object({ name: z.string() })) .mutation((opts) => { return { user: { name: opts.input.name, role: 'ADMIN' as const } }; }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: components/MyComponent.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; export function MyComponent() { const mutation = trpc.login.useMutation(); const handleLogin = () => { const name = 'John Doe'; mutation.mutate({ name }); }; return (

Login Form

{mutation.error &&

Something went wrong! {mutation.error.message}

}
); } ``` -------------------------------------------------- --- id: useQueries title: useQueries() sidebar_label: useQueries() slug: /client/react/useQueries --- The `useQueries` hook can be used to fetch a variable number of queries at the same time using only one hook call. The main use case for such a hook is to be able to fetch a number of queries, usually of the same type. For example if you fetch a list of todo ids, you can then map over them in a useQueries hook calling a byId endpoint that would fetch the details of each todo. :::note While fetching multiple types in a `useQueries` hook is possible, there is not much of an advantage compared to using multiple `useQuery` calls unless you use the `suspense` option as that `useQueries` can trigger suspense in parallel while multiple `useQuery` calls would waterfall. ::: ## Usage The useQueries hook is the same as that of [@tanstack/query useQueries](https://tanstack.com/query/v5/docs/framework/react/reference/useQueries). The only difference is that instead of passing an object with a `queries` array, you pass a callback function that receives a `t` proxy and returns an array of queries. :::tip When you're using the [`httpBatchLink`](/docs/client/links/httpBatchLink) or [`wsLink`](/docs/client/links/wsLink), the below will end up being only 1 HTTP call to your server. Additionally, if the underlying procedure is using something like Prisma's `findUnique()` it will [automatically batch](https://www.prisma.io/docs/guides/performance-and-optimization/query-optimization-performance#solving-n1-in-graphql-with-findunique-and-prismas-dataloader) & do exactly 1 database query as a well. ::: ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ byId: t.procedure .input(z.object({ id: z.string() })) .query(({ input }) => { return { id: input.id, title: 'Example Post' }; }), }), greeting: t.procedure .input(z.object({ text: z.string() })) .query(({ input }) => { return { message: `Hello ${input.text}` }; }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: component.tsx import React from 'react'; // ---cut--- import { trpc } from './utils/trpc'; const Component = (props: { postIds: string[] }) => { const postQueries = trpc.useQueries((t) => props.postIds.map((id) => t.post.byId({ id })), ); return <>{/* [...] */}; }; ``` ### Providing options to individual queries You can also pass in any normal query options to the second parameter of any of the query calls in the array such as `enabled`, `suspense`, `refetchOnWindowFocus`...etc. For a complete overview of all the available options, see the [tanstack useQuery](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery) documentation. ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ byId: t.procedure .input(z.object({ id: z.string() })) .query(({ input }) => { return { id: input.id, title: 'Example Post' }; }), }), greeting: t.procedure .input(z.object({ text: z.string() })) .query(({ input }) => { return { message: `Hello ${input.text}` }; }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: component.tsx import React from 'react'; // ---cut--- import { trpc } from './utils/trpc'; const Component = () => { const [post, greeting] = trpc.useQueries((t) => [ t.post.byId({ id: '1' }, { enabled: false }), t.greeting({ text: 'world' }), ]); const onButtonClick = () => { post.refetch(); }; return (

{post.data && post.data.title}

{greeting.data?.message}

); }; ``` ### Context You can also pass in an optional React Query context to override the default. ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ byId: t.procedure .input(z.object({ id: z.string() })) .query(({ input }) => { return { id: input.id, title: 'Example Post' }; }), }), greeting: t.procedure .input(z.object({ text: z.string() })) .query(({ input }) => { return { message: `Hello ${input.text}` }; }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: component.tsx import { trpc } from './utils/trpc'; declare const myCustomContext: any; // ---cut--- const [post, greeting] = trpc.useQueries( (t) => [t.post.byId({ id: '1' }), t.greeting({ text: 'world' })], myCustomContext, ); ``` -------------------------------------------------- --- id: useQuery title: useQuery() sidebar_label: useQuery() slug: /client/react/useQuery --- `useQuery` is the primary hook for data fetching, it works similarly to `@tanstack/react-query`'s `useQuery`, but with some `trpc` specific options and additional features like streaming. :::note For in-depth information about options and usage patterns, refer to the TanStack Query docs on [queries](https://tanstack.com/query/v5/docs/framework/react/guides/queries). ::: ## Signature ```tsx twoslash type TInput = unknown; type SkipToken = symbol; interface UseQueryOptions {} // ---cut--- declare function useQuery( input: TInput | SkipToken, opts?: UseTRPCQueryOptions, ): void; interface UseTRPCQueryOptions extends UseQueryOptions { trpc: { ssr?: boolean; abortOnUnmount?: boolean; context?: Record; } } ``` Since `UseTRPCQueryOptions` extends `@tanstack/react-query`'s `UseQueryOptions`, you can use any of their options here such as `enabled`, `refetchOnWindowFocus`, etc. We also have some `trpc` specific options that let you opt in or out of certain behaviors on a per-procedure level: - **`trpc.ssr`:** If you have `ssr: true` in your [global config](/docs/client/nextjs/pages-router/setup#ssr-boolean-default-false), you can set this to false to disable ssr for this particular query. _Note that this does not work the other way around, i.e., you can not enable ssr on a procedure if your global config is set to false._ - **`trpc.abortOnUnmount`:** Override the [global config](/docs/client/nextjs/pages-router/setup#config-callback) and opt in or out of aborting queries on unmount. - **`trpc.context`:** Add extra meta data that could be used in [Links](/docs/client/links). :::tip If you need to set any options but don't want to pass any input, you can pass `undefined` instead. ::: You'll notice that you get autocompletion on the `input` based on what you have set in your `input` schema on your backend. ## Example usage
Backend code ```tsx twoslash title='server/routers/_app.ts' import { initTRPC } from '@trpc/server'; import { z } from 'zod'; export const t = initTRPC.create(); export const appRouter = t.router({ // Create procedure at path 'hello' hello: t.procedure // using zod schema to validate and infer input values .input( z .object({ text: z.string().nullish(), }) .nullish(), ) .query((opts) => { return { greeting: `hello ${opts.input?.text ?? 'world'}`, }; }), }); export type AppRouter = typeof appRouter; ```
```tsx twoslash title='components/MyComponent.tsx' // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ hello: t.procedure .input(z.object({ text: z.string().nullish() }).nullish()) .query((opts) => { return { greeting: `hello ${opts.input?.text ?? 'world'}` }; }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: components/MyComponent.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; export function MyComponent() { // input is optional, so we don't have to pass second argument const helloNoArgs = trpc.hello.useQuery(); const helloWithArgs = trpc.hello.useQuery({ text: 'client' }); return (

Hello World Example

  • helloNoArgs ({helloNoArgs.status}):{' '}
    {JSON.stringify(helloNoArgs.data, null, 2)}
  • helloWithArgs ({helloWithArgs.status}):{' '}
    {JSON.stringify(helloWithArgs.data, null, 2)}
); } ``` ## Streaming responses using async generators {#streaming} :::info Since v11 we now support streaming queries when using the [`httpBatchStreamLink`](../links/httpBatchStreamLink.md#generators). ::: When returning an async generator in a query, you will: - Get the results of the iterator in the `data`-property **as an array** which updates as the response comes in - The `status` will be `success` as soon as the first chunk is received. - The `fetchStatus` property which will be `fetching` until the last chunk is received. ### Example ```tsx twoslash title='server/routers/_app.ts' import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const appRouter = t.router({ iterable: t.procedure.query(async function* () { for (let i = 0; i < 3; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); yield i; } }), }); export type AppRouter = typeof appRouter; ``` ```tsx twoslash title='components/MyComponent.tsx' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const appRouter = t.router({ iterable: t.procedure.query(async function* () { for (let i = 0; i < 3; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); yield i; } }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: components/MyComponent.tsx // ---cut--- import React, { Fragment } from 'react'; import { trpc } from '../utils/trpc'; export function MyComponent() { const result = trpc.iterable.useQuery(); return (
{result.data?.map((chunk, index) => ( {chunk} ))}
); } ``` `result` properties while streaming: | `status` | `fetchStatus` | `data` | | ----------- | ------------- | ----------- | | `'pending'` | `'fetching'` | `undefined` | | `'success'` | `'fetching'` | `[]` | | `'success'` | `'fetching'` | `[0]` | | `'success'` | `'fetching'` | `[0, 1]` | | `'success'` | `'fetching'` | `[0, 1, 2]` | | `'success'` | `'idle'` | `[0, 1, 2]` | -------------------------------------------------- --- id: useSubscription title: useSubscription() sidebar_label: useSubscription() slug: /client/react/useSubscription --- The `useSubscription` hook can be used to subscribe to a [subscription](../../server/subscriptions.md) procedure on the server. ## Signature ### Options :::tip - If you need to set any options but don't want to pass any input, you can pass `undefined` instead. - If you pass `skipToken` from `@tanstack/react-query`, the subscription will be paused. - Have a look at our [SSE example](https://github.com/trpc/examples-next-sse-chat) for a complete example of how to use subscriptions ::: ```tsx twoslash interface UseTRPCSubscriptionOptions { /** * Called when the subscription is started. */ onStarted?: () => void; /** * Called when new data is received from the subscription. */ onData?: (data: TOutput) => void; /** * Called when an **unrecoverable error** occurs and the subscription is stopped. */ onError?: (error: TError) => void; /** * Called when the subscription is completed on the server. * The state will transition to `'idle'` with `data: undefined`. */ onComplete?: () => void; /** * @deprecated Use a `skipToken` from `@tanstack/react-query` instead. * This will be removed in v12. */ enabled?: boolean; } ``` ### Return type The return type is a discriminated union on `status`: ```ts twoslash type TRPCSubscriptionResult = | TRPCSubscriptionIdleResult | TRPCSubscriptionConnectingResult | TRPCSubscriptionPendingResult | TRPCSubscriptionErrorResult; interface TRPCSubscriptionIdleResult { /** Subscription is disabled or has ended */ status: 'idle'; data: undefined; error: null; reset: () => void; } interface TRPCSubscriptionConnectingResult { /** Trying to establish a connection (may have a previous error from a reconnection attempt) */ status: 'connecting'; data: TOutput | undefined; error: TError | null; reset: () => void; } interface TRPCSubscriptionPendingResult { /** Connected to the server, receiving data */ status: 'pending'; data: TOutput | undefined; error: null; reset: () => void; } interface TRPCSubscriptionErrorResult { /** An unrecoverable error occurred and the subscription is stopped */ status: 'error'; data: TOutput | undefined; error: TError; reset: () => void; } ``` ## Example Procedure ```tsx twoslash title='server/routers/_app.ts' // @target: esnext // @types: node import EventEmitter, { on } from 'events'; import { initTRPC } from '@trpc/server'; export const t = initTRPC.create(); type Post = { id: string; title: string }; const ee = new EventEmitter(); export const appRouter = t.router({ onPostAdd: t.procedure.subscription(async function* (opts) { for await (const [data] of on(ee, 'add', { signal: opts.signal, })) { const post = data as Post; yield post; } }), }); export type AppRouter = typeof appRouter; ``` ## Example React Component ```tsx twoslash title='components/PostFeed.tsx' // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); type Post = { id: string; title: string }; const appRouter = t.router({ onPostAdd: t.procedure.subscription(async function* () { yield { id: '1', title: 'Hello' } as Post; }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: components/PostFeed.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; type Post = { id: string; title: string }; export function PostFeed() { const [posts, setPosts] = React.useState([]); const subscription = trpc.onPostAdd.useSubscription(undefined, { onData: (post) => { setPosts((prev) => [...prev, post]); }, }); return (

Live Feed

{subscription.status === 'connecting' &&

Connecting...

} {subscription.status === 'error' && (

Error: {subscription.error.message}

)}
    {posts.map((post) => (
  • {post.title}
  • ))}
); } ``` -------------------------------------------------- --- id: useUtils title: useUtils sidebar_label: useUtils() slug: /client/react/useUtils --- `useUtils` is a hook that gives you access to helpers that let you manage the cached data of the queries you execute via `@trpc/react-query`. These helpers are actually thin wrappers around `@tanstack/react-query`'s [`queryClient`](https://tanstack.com/query/v5/docs/reference/QueryClient) methods. If you want more in-depth information about options and usage patterns for `useUtils` helpers than what we provide here, we will link to their respective `@tanstack/react-query` docs so you can refer to them accordingly. :::note This hook was called `useContext()` until `10.41.0` (and is still aliased for the foreseeable future) ::: ## Usage `useUtils` returns an object with all the available queries you have in your routers. You use it the same way as your `trpc` client object. Once you reach a query, you'll have access to the query helpers. For example, let's say you have a `post` router with an `all` query: ```twoslash include server // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ all: t.procedure.query(() => { return { posts: [ { id: 1, title: 'everlong' }, { id: 2, title: 'After Dark' }, ], }; }), }), }); export type AppRouter = typeof appRouter; ``` ```ts twoslash title='server.ts' // @target: esnext // @filename: server.ts // ---cut--- import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ all: t.procedure.query(() => { return { posts: [ { id: 1, title: 'everlong' }, { id: 2, title: 'After Dark' }, ], }; }), }), }); export type AppRouter = typeof appRouter; ``` Now in our component, when we navigate the object `useUtils` gives us and reach the `post.all` query, we'll get access to our query helpers! ```tsx twoslash title="MyComponent.tsx" // @target: esnext // @include: server // @filename: MyComponent.tsx // ---cut--- // @errors: 2339 import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from './server'; const trpc = createTRPCReact(); function MyComponent() { const utils = trpc.useUtils(); utils.post.all.f; // ^| } ``` ## Helpers These are the helpers you'll get access to via `useUtils`. The table below will help you know which tRPC helper wraps which `@tanstack/react-query` helper method. Each react-query method will link to its respective docs/guide: | tRPC helper wrapper | `@tanstack/react-query` helper method | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `fetch` | [`queryClient.fetchQuery`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientfetchquery) | | `prefetch` | [`queryClient.prefetchQuery`](https://tanstack.com/query/v5/docs/framework/react/guides/prefetching) | | `fetchInfinite` | [`queryClient.fetchInfiniteQuery`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientfetchinfinitequery) | | `prefetchInfinite` | [`queryClient.prefetchInfiniteQuery`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientprefetchinfinitequery) | | `ensureData` | [`queryClient.ensureData`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientensurequerydata) | | `invalidate` | [`queryClient.invalidateQueries`](https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation) | | `refetch` | [`queryClient.refetchQueries`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientrefetchqueries) | | `cancel` | [`queryClient.cancelQueries`](https://tanstack.com/query/v5/docs/framework/react/guides/query-cancellation) | | `setData` | [`queryClient.setQueryData`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientsetquerydata) | | `setQueriesData` | [`queryClient.setQueriesData`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientsetqueriesdata) | | `getData` | [`queryClient.getQueryData`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientgetquerydata) | | `setInfiniteData` | [`queryClient.setInfiniteQueryData`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientsetquerydata) | | `getInfiniteData` | [`queryClient.getInfiniteData`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientgetquerydata) | | `setMutationDefaults` | [`queryClient.setMutationDefaults`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientsetmutationdefaults) | | `getMutationDefaults` | [`queryClient.getMutationDefaults`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientgetmutationdefaults) | | `isMutating` | [`queryClient.isMutating`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientismutating) | | `reset` | [`queryClient.resetQueries`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientresetqueries) | ### ❓ The function I want isn't here! `@tanstack/react-query` has a lot of functions that we haven't put in the tRPC context yet. If you need a function that isn't here, feel free to [open a feature request](https://github.com/trpc/trpc/issues/new/choose) requesting it. In the meantime, you can import and use the function directly from `@tanstack/react-query`. We also provide a [getQueryKey](/docs/client/react/getQueryKey) which you can use to get the correct queryKey on the filters when using these functions. ## Proxy client In addition to the above react-query helpers, the context also exposes your tRPC proxy client. This lets you call your procedures with `async`/`await` without needing to create an additional vanilla client. ```tsx twoslash // @jsx: react-jsx // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({ apiKey: t.router({ create: t.procedure.mutation(() => 'new-api-key'), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: MyComponent.tsx // ---cut--- import { useState } from 'react'; import { trpc } from './utils/trpc'; function MyComponent() { const [apiKey, setApiKey] = useState(''); const utils = trpc.useUtils(); return (
{ const apiKey = await utils.client.apiKey.create.mutate(); setApiKey(apiKey); }} > {/* form content */}
); } ``` ## Query Invalidation You invalidate queries via the `invalidate` helper. `invalidate` is actually a special helper given that, unlike the other helpers, it's available at every level of the router map. This means you can either run `invalidate` on a single query, a whole router, or every router if you want. We get more in detail in the sections below. ### Invalidating a single query You can invalidate a query relating to a single procedure and even filter based on the input passed to it to prevent unnecessary calls to the back end. #### Example code ```tsx twoslash // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ all: t.procedure.query(() => { return { posts: [{ id: 1, title: 'everlong' }] }; }), byId: t.procedure .input(z.object({ id: z.number() })) .query(({ input }) => ({ post: { id: input.id, title: 'Look me up!' } })), edit: t.procedure .input(z.object({ id: z.number(), title: z.string() })) .mutation(({ input }) => ({ id: input.id, title: input.title })), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: MyComponent.tsx // ---cut--- import { trpc } from './utils/trpc'; function MyComponent() { const utils = trpc.useUtils(); const mutation = trpc.post.edit.useMutation({ onSuccess(input) { utils.post.all.invalidate(); utils.post.byId.invalidate({ id: input.id }); // Will not invalidate queries for other id's }, }); // [...] } ``` ### Invalidating across whole routers It is also possible to invalidate queries across an entire router rather than just one query. #### Example code
Backend code ```tsx twoslash title='server/routers/_app.ts' import { initTRPC } from '@trpc/server'; import { z } from 'zod'; export const t = initTRPC.create(); export const appRouter = t.router({ // sub Post router post: t.router({ all: t.procedure.query(() => { return { posts: [ { id: 1, title: 'everlong' }, { id: 2, title: 'After Dark' }, ], }; }), byId: t.procedure .input( z.object({ id: z.string(), }), ) .query(({ input }) => { return { post: { id: input?.id, title: 'Look me up!' }, }; }), edit: t.procedure .input(z.object({ id: z.number(), title: z.string() })) .mutation(({ input }) => { return { post: { id: input.id, title: input.title } }; }), }), // separate user router user: t.router({ all: t.procedure.query(() => { return { users: [{ name: 'Dave Grohl' }, { name: 'Haruki Murakami' }] }; }), }), }); ```
```tsx twoslash // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ all: t.procedure.query(() => ({ posts: [{ id: 1, title: 'everlong' }, { id: 2, title: 'After Dark' }], })), byId: t.procedure .input(z.object({ id: z.number() })) .query(({ input }) => ({ post: { id: input.id, title: 'Look me up!' } })), edit: t.procedure .input(z.object({ id: z.number(), title: z.string() })) .mutation(({ input }) => ({ post: { id: input.id, title: input.title } })), }), user: t.router({ all: t.procedure.query(() => ({ users: [{ name: 'Dave Grohl' }, { name: 'Haruki Murakami' }], })), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: MyComponent.tsx // ---cut--- import { trpc } from './utils/trpc'; function MyComponent() { const utils = trpc.useUtils(); const invalidateAllQueriesAcrossAllRouters = () => { // 1️⃣ // All queries on all routers will be invalidated utils.invalidate(); }; const invalidateAllPostQueries = () => { // 2️⃣ // All post queries will be invalidated utils.post.invalidate(); }; const invalidatePostById = () => { // 3️⃣ // All queries in the post router with input {id:1} invalidated utils.post.byId.invalidate({ id: 1 }); }; // Example queries trpc.user.all.useQuery(); // Would only be validated by 1️⃣ only. trpc.post.all.useQuery(); // Would be invalidated by 1️⃣ & 2️⃣ trpc.post.byId.useQuery({ id: 1 }); // Would be invalidated by 1️⃣, 2️⃣ and 3️⃣ trpc.post.byId.useQuery({ id: 2 }); // would be invalidated by 1️⃣ and 2️⃣ but NOT 3️⃣! // [...] } ``` ### Invalidate full cache on every mutation Keeping track of exactly what queries a mutation should invalidate is hard, therefore, it can be a pragmatic solution to invalidate the _full cache_ as a side-effect on any mutation. Since we have request batching, this invalidation will simply refetch all queries on the page you're looking at in one single request. We have added a feature to help with this: ```ts twoslash // @target: esnext // @include: server // @filename: utils/trpc.ts // ---cut--- import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact({ overrides: { useMutation: { /** * This function is called whenever a `.useMutation` succeeds **/ async onSuccess(opts) { /** * @note that order here matters: * The order here allows route changes in `onSuccess` without * having a flash of content change whilst redirecting. **/ // Calls the `onSuccess` defined in the `useQuery()`-options: await opts.originalFn(); // Invalidate all queries in the react-query cache: await opts.queryClient.invalidateQueries(); }, }, }, }); ``` ## Additional Options Aside from the query helpers, the object `useUtils` returns also contains the following properties: ```ts twoslash type AnyRouter = any; type TRPCClient = any; type SSRState = false | 'prepass' | 'mounting' | 'mounted'; // ---cut--- interface ProxyTRPCContextProps { /** * The `TRPCClient` */ client: TRPCClient; /** * The SSR context when server-side rendering * @default null */ ssrContext?: TSSRContext | null; /** * State of SSR hydration. * - `false` if not using SSR. * - `prepass` when doing a prepass to fetch queries' data * - `mounting` before TRPCProvider has been rendered on the client * - `mounted` when the TRPCProvider has been rendered on the client * @default false */ ssrState?: SSRState; /** * Abort loading query calls when unmounting a component - usually when navigating to a new page * @default false */ abortOnUnmount?: boolean; } ``` -------------------------------------------------- --- id: migrating title: Migrating from the classic React Client sidebar_label: Migrating description: Migrating from the classic React Client slug: /client/tanstack-react-query/migrating --- There are a few approaches to migrate over, and this library is a significant departure from the classic client, so we're not expecting anybody to do it in one shot. But you will probably want to try a combination of... ## Codemod migration :::info The codemod is a work in progress and we're looking for help to make it better. If you're interested in contributing to the codemod, please see [Julius' comment here](https://github.com/trpc/trpc/pull/6262#issuecomment-2651959435). ::: We're working on a codemod to help you migrate your existing codebase over to the new client. This is already available to try but we need your feedback and contributions to improve it. Codemods are very tricky to get right so we're looking for your help to make it as effective as possible. Run our upgrade CLI: ```sh npx @trpc/upgrade ``` When prompted, select the transforms `Migrate Hooks to xxxOptions API` and `Migrate context provider setup`. ## Gradual migration The new and classic clients are compatible with each other and [can live together in the same application](https://github.com/juliusmarminge/trpc-interop/blob/main/src/client.tsx). This means you can start migrating by using the new client in new parts of your application, and gradually migrate over existing usage as you see fit. Most importantly, Query Keys are identical, which means you can use the new client and classic client together and still rely on TanStack Query's caching. ### Migrating Queries A classic query would look like this ```tsx twoslash // @filename: trpc.ts import type { UseQueryResult } from '@tanstack/react-query'; declare const trpc: { greeting: { useQuery: (input: { name: string }) => UseQueryResult<`Hello ${string}`>; }; }; export { trpc }; // @filename: component.tsx // ---cut--- import { trpc } from './trpc'; function Users() { const greetingQuery = trpc.greeting.useQuery({ name: 'Jerry' }); // greetingQuery.data === 'Hello Jerry' } ``` and changes to ```tsx twoslash // @filename: server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ greeting: t.procedure.input(z.object({ name: z.string() })).query(({ input }) => `Hello ${input.name}` as const), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server/router'; export const { TRPCProvider, useTRPC } = createTRPCContext(); // @filename: component.tsx // ---cut--- import { useQuery } from '@tanstack/react-query'; import { useTRPC } from './trpc'; function Users() { const trpc = useTRPC(); const greetingQuery = useQuery(trpc.greeting.queryOptions({ name: 'Jerry' })); // greetingQuery.data === 'Hello Jerry' } ``` ### Migrating Invalidations and other QueryClient usages A classic invalidation pattern would look like this ```tsx twoslash // @filename: trpc.ts declare const trpc: { useUtils: () => { greeting: { invalidate: (input: { name: string }) => Promise; }; }; }; export { trpc }; // @filename: component.tsx // ---cut--- import { trpc } from './trpc'; function Users() { const utils = trpc.useUtils(); async function invalidateGreeting() { await utils.greeting.invalidate({ name: 'Jerry' }); } } ``` and changes to ```tsx twoslash // @filename: server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ greeting: t.procedure.input(z.object({ name: z.string() })).query(({ input }) => `Hello ${input.name}` as const), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server/router'; export const { TRPCProvider, useTRPC } = createTRPCContext(); // @filename: component.tsx // ---cut--- import { useQueryClient } from '@tanstack/react-query'; import { useTRPC } from './trpc'; function Users() { const trpc = useTRPC(); const queryClient = useQueryClient(); async function invalidateGreeting() { await queryClient.invalidateQueries( trpc.greeting.queryFilter({ name: 'Jerry' }), ); } } ``` This is the same for any QueryClient usage, instead of using tRPC's `useUtils` you can now follow the TanStack documentation directly ### Migrating Mutations A classic mutation might look like this ```tsx twoslash // @filename: trpc.ts import type { UseMutationResult } from '@tanstack/react-query'; declare const trpc: { createUser: { useMutation: () => UseMutationResult; }; }; export { trpc }; // @filename: component.tsx // ---cut--- import { trpc } from './trpc'; function Users() { const createUserMutation = trpc.createUser.useMutation(); createUserMutation.mutate({ name: 'Jerry' }); } ``` and changes to ```tsx twoslash // @filename: server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ createUser: t.procedure.input(z.object({ name: z.string() })).mutation(() => 'created'), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server/router'; export const { TRPCProvider, useTRPC } = createTRPCContext(); // @filename: component.tsx // ---cut--- import { useMutation } from '@tanstack/react-query'; import { useTRPC } from './trpc'; function Users() { const trpc = useTRPC(); const createUserMutation = useMutation(trpc.createUser.mutationOptions()); createUserMutation.mutate({ name: 'Jerry' }); } ``` -------------------------------------------------- --- id: overview title: TanStack React Query sidebar_label: Overview description: TanStack React Query integration for tRPC slug: /client/tanstack-react-query --- This is the recommended way to use tRPC with React and TanStack Query. It provides factories for common TanStack React Query interfaces like `QueryKeys`, `QueryOptions`, and `MutationOptions`, giving you a more TanStack Query-native experience. If you're starting a new project, we recommend using this integration over the [classic React Query integration](/docs/client/react). Read the [announcement post](/blog/introducing-tanstack-react-query-client) for more information about this change. Head to the [Setup](/docs/client/tanstack-react-query/setup) guide to get started. -------------------------------------------------- --- id: server-components title: Set up with React Server Components sidebar_label: Server Components slug: /client/tanstack-react-query/server-components --- import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs'; :::tip **Using Next.js?** See the dedicated [Next.js App Router setup guide](/docs/client/nextjs/app-router-setup) for a streamlined walkthrough tailored to Next.js. ::: This guide is an overview of how one may use tRPC with a React Server Components (RSC) framework such as Next.js App Router. Be aware that RSC on its own solves a lot of the same problems tRPC was designed to solve, so you may not need tRPC at all. There is also not a one-size-fits-all way to integrate tRPC with RSCs, so see this guide as a starting point and adjust it to your needs and preferences. :::info If you're looking for how to use tRPC with Server Actions, see the [Server Actions guide](/docs/client/nextjs/server-actions). ::: :::caution Please read React Query's [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr) docs before proceeding to understand the different types of server rendering and what footguns to avoid. ::: ## Add tRPC to existing projects ### 1. Install deps import { InstallSnippet } from '@site/src/components/InstallSnippet'; ### 2. Create a tRPC router Initialize your tRPC backend in `trpc/init.ts` using the `initTRPC` function, and create your first router. We're going to make a simple "hello world" router and procedure here - but for deeper information on creating your tRPC API you should refer to the [Quickstart guide](/docs/quickstart) and [Backend usage docs](/docs/server/overview) for tRPC information. :::info The file names used here are not enforced by tRPC. You may use any file structure you wish. :::
View sample backend ```ts twoslash title='trpc/init.ts' import { initTRPC } from '@trpc/server'; import { cache } from 'react'; export const createTRPCContext = cache(async () => { /** * @see: https://trpc.io/docs/server/context */ return { userId: 'user_123' }; }); // Avoid exporting the entire t-object // since it's not very descriptive. // For instance, the use of a t variable // is common in i18n libraries. const t = initTRPC.create({ /** * @see https://trpc.io/docs/server/data-transformers */ // transformer: superjson, }); // Base router and procedure helpers export const createTRPCRouter = t.router; export const createCallerFactory = t.createCallerFactory; export const baseProcedure = t.procedure; ```
```ts twoslash title='trpc/routers/_app.ts' // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCRouter = t.router; export const baseProcedure = t.procedure; // @filename: trpc/routers/_app.ts // ---cut--- import { z } from 'zod'; import { baseProcedure, createTRPCRouter } from '../init'; export const appRouter = createTRPCRouter({ hello: baseProcedure .input( z.object({ text: z.string(), }), ) .query((opts) => { return { greeting: `hello ${opts.input.text}`, }; }), }); // export type definition of API export type AppRouter = typeof appRouter; ```
:::note The backend adapter depends on your framework and how it sets up API routes. The following example sets up GET and POST routes at `/api/trpc/*` using the [fetch adapter](https://trpc.io/docs/server/adapters/fetch) in Next.js. ::: ```ts twoslash title='app/api/trpc/[trpc]/route.ts' // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCContext = async () => ({ userId: 'user_123' }); export const createTRPCRouter = t.router; export const baseProcedure = t.procedure; // @filename: trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string() })).query((opts) => ({ greeting: `hello ${opts.input.text}`, })), }); export type AppRouter = typeof appRouter; // @filename: app/api/trpc/[trpc]/route.ts // ---cut--- import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { createTRPCContext } from '../../../../trpc/init'; import { appRouter } from '../../../../trpc/routers/_app'; const handler = (req: Request) => fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext: createTRPCContext, }); export { handler as GET, handler as POST }; ```
### 3. Create a Query Client factory Create a shared file `trpc/query-client.ts` that exports a function that creates a `QueryClient` instance. ```ts twoslash title='trpc/query-client.ts' import { defaultShouldDehydrateQuery, QueryClient, } from '@tanstack/react-query'; import superjson from 'superjson'; export function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 30 * 1000, }, dehydrate: { // serializeData: superjson.serialize, shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', }, hydrate: { // deserializeData: superjson.deserialize, }, }, }); } ``` We're setting a few default options here: - `staleTime`: With SSR, we usually want to set some default staleTime above 0 to avoid refetching immediately on the client. - `shouldDehydrateQuery`: This is a function that determines whether a query should be dehydrated or not. Since the RSC transport protocol supports hydrating promises over the network, we extend the `defaultShouldDehydrateQuery` function to also include queries that are still pending. This will allow us to start prefetching in a server component high up the tree, then consuming that promise in a client component further down. - `serializeData` and `deserializeData` (optional): If you set up a [data transformer](https://trpc.io/docs/server/data-transformers) in the previous step, set this option to make sure the data is serialized correctly when hydrating the query client over the server-client boundary. ### 4. Create a tRPC client for Client Components The `trpc/client.tsx` is the entrypoint when consuming your tRPC API from client components. In here, import the **type definition** of your tRPC router and create typesafe hooks using `createTRPCContext`. We'll also export our context provider from this file. ```tsx twoslash title='trpc/client.tsx' // @target: esnext // @jsx: react-jsx // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCRouter = t.router; export const baseProcedure = t.procedure; // @filename: trpc/routers/_app.ts import { z } from 'zod'; import { baseProcedure, createTRPCRouter } from '../init'; export const appRouter = createTRPCRouter({ hello: baseProcedure.input(z.object({ text: z.string() })).query((opts) => ({ greeting: `hello ${opts.input.text}`, })), }); export type AppRouter = typeof appRouter; // @filename: trpc/query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: trpc/client.tsx // ---cut--- 'use client'; // ^-- to make sure we can mount the Provider from a server component import type { QueryClient } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query'; import { createTRPCClient, httpBatchLink } from '@trpc/client'; import { createTRPCContext } from '@trpc/tanstack-react-query'; import { useState } from 'react'; import { makeQueryClient } from './query-client'; import type { AppRouter } from './routers/_app'; export const { TRPCProvider, useTRPC } = createTRPCContext(); let browserQueryClient: QueryClient; function getQueryClient() { if (typeof window === 'undefined') { // Server: always make a new query client return makeQueryClient(); } // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient(); return browserQueryClient; } function getUrl() { const base = (() => { if (typeof window !== 'undefined') return ''; if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; return 'http://localhost:3000'; })(); return `${base}/api/trpc`; } export function TRPCReactProvider( props: Readonly<{ children: React.ReactNode; }>, ) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient(); const [trpcClient] = useState(() => createTRPCClient({ links: [ httpBatchLink({ // transformer: superjson, <-- if you use a data transformer url: getUrl(), }), ], }), ); return ( {props.children} ); } ``` Mount the provider in the root of your application (e.g. `app/layout.tsx` when using Next.js). ### 5. Create a tRPC caller for Server Components To prefetch queries from server components, we create a proxy from our router. You can also pass in a client if your router is on a separate server. ```tsx twoslash title='trpc/server.tsx' // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCContext = async () => ({ userId: 'user_123' }); export const createTRPCRouter = t.router; export const baseProcedure = t.procedure; // @filename: trpc/query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string() })).query((opts) => ({ greeting: `hello ${opts.input.text}`, })), }); export type AppRouter = typeof appRouter; // @filename: server-only.d.ts declare module 'server-only' {} // @filename: trpc/server.tsx // ---cut--- import 'server-only'; // <-- ensure this file cannot be imported from the client import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; import { createTRPCClient, httpLink } from '@trpc/client'; import { cache } from 'react'; import { createTRPCContext } from './init'; import { makeQueryClient } from './query-client'; import { appRouter } from './routers/_app'; import type { AppRouter } from './routers/_app'; // IMPORTANT: Create a stable getter for the query client that // will return the same client during the same request. export const getQueryClient = cache(makeQueryClient); export const trpc = createTRPCOptionsProxy({ ctx: createTRPCContext, router: appRouter, queryClient: getQueryClient, }); // If your router is on a separate server, pass a client: createTRPCOptionsProxy({ client: createTRPCClient({ links: [httpLink({ url: '...' })], }), queryClient: getQueryClient, }); ``` ## Using your API Now you can use your tRPC API in your app. While you can use the React Query hooks in client components just like you would in any other React app, we can take advantage of the RSC capabilities by prefetching queries in a server component high up the tree. You may be familiar with this concept as "render as you fetch" commonly implemented as loaders. This means the request fires as soon as possible but without suspending until the data is needed by using the `useQuery` or `useSuspenseQuery` hooks. This approach leverages Next.js App Router's streaming capabilities, initiating the query on the server and streaming data to the client as it becomes available. It optimizes both the time to first byte in the browser and the data fetch time, resulting in faster page loads. However, `greeting.data` may initially be `undefined` before the data streams in. If you prefer to avoid this initial undefined state, you can `await` the `prefetchQuery` call. This ensures the query on the client always has data on first render, but it comes with a tradeoff - the page will load more slowly since the server must complete the query before sending HTML to the client. ```tsx twoslash title='app/page.tsx' // @jsx: react-jsx // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCContext = async () => ({ userId: 'user_123' }); export const createTRPCRouter = t.router; export const baseProcedure = t.procedure; // @filename: trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string().optional() }).optional()).query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}`, })), }); export type AppRouter = typeof appRouter; // @filename: trpc/query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: trpc/server.tsx import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; import { cache } from 'react'; import { createTRPCContext } from './init'; import { makeQueryClient } from './query-client'; import { appRouter } from './routers/_app'; export const getQueryClient = cache(makeQueryClient); export const trpc = createTRPCOptionsProxy({ ctx: createTRPCContext, router: appRouter, queryClient: getQueryClient, }); // @filename: app/client-greeting.tsx export function ClientGreeting() { return null; } // @filename: app/page.tsx // ---cut--- import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; import { getQueryClient, trpc } from '../trpc/server'; import { ClientGreeting } from './client-greeting'; export default async function Home() { const queryClient = getQueryClient(); void queryClient.prefetchQuery( trpc.hello.queryOptions({ /** input */ }), ); return (
...
{/** ... */}
); } ``` ```tsx twoslash title='app/client-greeting.tsx' // @jsx: react-jsx // @filename: server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string() })).query((opts) => ({ greeting: `hello ${opts.input.text}`, })), }); export type AppRouter = typeof appRouter; // @filename: trpc/client.tsx import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from '../server/router'; export const { TRPCProvider, useTRPC } = createTRPCContext(); // @filename: app/client-greeting.tsx // ---cut--- 'use client'; // <-- hooks can only be used in client components import { useQuery } from '@tanstack/react-query'; import { useTRPC } from '../trpc/client'; export function ClientGreeting() { const trpc = useTRPC(); const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' })); if (!greeting.data) return
Loading...
; return
{greeting.data.greeting}
; } ``` :::tip You can also create `prefetch` and `HydrateClient` helper functions to make it a bit more concise and reusable: ```tsx twoslash title='trpc/server.tsx' // @jsx: react-jsx // @filename: trpc/query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: trpc/server.tsx import { cache } from 'react'; import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; import type { TRPCQueryOptions } from '@trpc/tanstack-react-query'; import { makeQueryClient } from './query-client'; const getQueryClient = cache(makeQueryClient); // ---cut--- export function HydrateClient(props: { children: React.ReactNode }) { const queryClient = getQueryClient(); return ( {props.children} ); } export function prefetch>>( queryOptions: T, ) { const queryClient = getQueryClient(); if (queryOptions.queryKey[1]?.type === 'infinite') { void queryClient.prefetchInfiniteQuery(queryOptions as any); } else { void queryClient.prefetchQuery(queryOptions); } } ``` Then you can use it like this: ```tsx twoslash // @jsx: react-jsx // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCContext = async () => ({ userId: 'user_123' }); export const createTRPCRouter = t.router; export const baseProcedure = t.procedure; // @filename: trpc/query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string().optional() }).optional()).query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}`, })), }); export type AppRouter = typeof appRouter; // @filename: trpc/server.tsx import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; import type { TRPCQueryOptions } from '@trpc/tanstack-react-query'; import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; import { cache } from 'react'; import { createTRPCContext } from './init'; import { makeQueryClient } from './query-client'; import { appRouter } from './routers/_app'; export const getQueryClient = cache(makeQueryClient); export const trpc = createTRPCOptionsProxy({ ctx: createTRPCContext, router: appRouter, queryClient: getQueryClient }); export function HydrateClient(props: { children: React.ReactNode }) { const queryClient = getQueryClient(); return {props.children}; } export function prefetch>>(queryOptions: T) { const queryClient = getQueryClient(); void queryClient.prefetchQuery(queryOptions); } // @filename: app/client-greeting.tsx export function ClientGreeting() { return null; } // @filename: app/page.tsx // ---cut--- import { HydrateClient, prefetch, trpc } from '../trpc/server'; import { ClientGreeting } from './client-greeting'; function Home() { prefetch( trpc.hello.queryOptions({ /** input */ }), ); return (
...
{/** ... */}
); } ``` ::: ### Leveraging Suspense You may prefer handling loading and error states using Suspense and Error Boundaries. You can do this by using the `useSuspenseQuery` hook. ```tsx twoslash title='app/page.tsx' // @jsx: react-jsx // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCContext = async () => ({ userId: 'user_123' }); // @filename: trpc/query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string().optional() }).optional()).query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}`, })), }); export type AppRouter = typeof appRouter; // @filename: trpc/server.tsx import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; import type { TRPCQueryOptions } from '@trpc/tanstack-react-query'; import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; import { cache } from 'react'; import { createTRPCContext } from './init'; import { makeQueryClient } from './query-client'; import { appRouter } from './routers/_app'; export const getQueryClient = cache(makeQueryClient); export const trpc = createTRPCOptionsProxy({ ctx: createTRPCContext, router: appRouter, queryClient: getQueryClient }); export function HydrateClient(props: { children: React.ReactNode }) { const queryClient = getQueryClient(); return {props.children}; } export function prefetch>>(queryOptions: T) { const queryClient = getQueryClient(); void queryClient.prefetchQuery(queryOptions); } // @filename: app/client-greeting.tsx export function ClientGreeting() { return null; } // @filename: app/page.tsx // ---cut--- import { HydrateClient, prefetch, trpc } from '../trpc/server'; import { Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { ClientGreeting } from './client-greeting'; export default async function Home() { prefetch(trpc.hello.queryOptions()); return (
...
{/** ... */} Something went wrong}> Loading...}>
); } ``` ```tsx twoslash title='app/client-greeting.tsx' // @jsx: react-jsx // @filename: server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string().optional() }).optional()).query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}`, })), }); export type AppRouter = typeof appRouter; // @filename: trpc/client.tsx import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from '../server/router'; export const { TRPCProvider, useTRPC } = createTRPCContext(); // @filename: app/client-greeting.tsx // ---cut--- 'use client'; import { useSuspenseQuery } from '@tanstack/react-query'; import { useTRPC } from '../trpc/client'; export function ClientGreeting() { const trpc = useTRPC(); const { data } = useSuspenseQuery(trpc.hello.queryOptions()); return
{data.greeting}
; } ``` ### Getting data in a server component If you need access to the data in a server component, we recommend creating a server caller and using it directly. Please note that this method is detached from your query client and does not store the data in the cache. This means that you cannot use the data in a server component and expect it to be available in the client. This is intentional and explained in more detail in the [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#data-ownership-and-revalidation) guide. ```tsx twoslash title='trpc/server.tsx' // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCContext = async () => ({ userId: 'user_123' }); // @filename: trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string().optional() }).optional()).query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}`, })), }); export type AppRouter = typeof appRouter; // @filename: trpc/server.tsx import { createTRPCContext } from './init'; import { appRouter } from './routers/_app'; // ---cut--- // ... export const caller = appRouter.createCaller(createTRPCContext); ``` ```tsx twoslash title='app/page.tsx' // @jsx: react-jsx // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCContext = async () => ({ userId: 'user_123' }); // @filename: trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string().optional() }).optional()).query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}`, })), }); export type AppRouter = typeof appRouter; // @filename: trpc/server.tsx import { createTRPCContext } from './init'; import { appRouter } from './routers/_app'; export const caller = appRouter.createCaller(createTRPCContext); // @filename: app/page.tsx // ---cut--- import { caller } from '../trpc/server'; export default async function Home() { const greeting = await caller.hello(); // ^? { greeting: string } return
{greeting.greeting}
; } ``` If you **really** need to use the data both on the server as well as inside client components and understand the tradeoffs explained in the [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#data-ownership-and-revalidation) guide, you can use `fetchQuery` instead of `prefetch` to have the data both on the server as well as hydrating it down to the client: ```tsx twoslash title='app/page.tsx' // @jsx: react-jsx // @filename: trpc/init.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const createTRPCContext = async () => ({ userId: 'user_123' }); // @filename: trpc/query-client.ts import { QueryClient } from '@tanstack/react-query'; export function makeQueryClient() { return new QueryClient(); } // @filename: trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ hello: t.procedure.input(z.object({ text: z.string().optional() }).optional()).query((opts) => ({ greeting: `hello ${opts.input?.text ?? 'world'}`, })), }); export type AppRouter = typeof appRouter; // @filename: trpc/server.tsx import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; import type { TRPCQueryOptions } from '@trpc/tanstack-react-query'; import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; import { cache } from 'react'; import { createTRPCContext } from './init'; import { makeQueryClient } from './query-client'; import { appRouter } from './routers/_app'; export const getQueryClient = cache(makeQueryClient); export const trpc = createTRPCOptionsProxy({ ctx: createTRPCContext, router: appRouter, queryClient: getQueryClient }); export function HydrateClient(props: { children: React.ReactNode }) { const queryClient = getQueryClient(); return {props.children}; } // @filename: app/client-greeting.tsx export function ClientGreeting() { return null; } // @filename: app/page.tsx // ---cut--- import { getQueryClient, HydrateClient, trpc } from '../trpc/server'; import { ClientGreeting } from './client-greeting'; export default async function Home() { const queryClient = getQueryClient(); const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions()); // Do something with greeting on the server return (
...
{/** ... */}
); } ``` -------------------------------------------------- --- id: setup title: TanStack React Query sidebar_label: Setup description: TanStack React Query setup slug: /client/tanstack-react-query/setup --- Compared to our [classic React Query Integration](/docs/client/react) this client is simpler and more TanStack Query-native, providing factories for common TanStack React Query interfaces like QueryKeys, QueryOptions, and MutationOptions. We think it's the future and recommend using this over the classic client, read the announcement post for more information about this change. :::tip You can try this integration out on the homepage of tRPC.io: [https://trpc.io/?try=minimal-react#try-it-out](/?try=minimal-react#try-it-out) :::
❓ Do I have to use an integration? No! The integration is fully optional. You can use `@tanstack/react-query` using just a [vanilla tRPC client](/docs/client/vanilla), although then you'll have to manually manage query keys and do not get the same level of DX as when using the integration package. ```ts twoslash title='utils/trpc.ts' // @filename: server/router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ list: t.procedure.query(() => [{ id: '1', title: 'Hello' }]), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../server/router'; export const trpc = createTRPCClient({ links: [httpBatchLink({ url: 'YOUR_API_URL' })], }); ``` ```tsx twoslash title='components/PostList.tsx' // @filename: server/router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ list: t.procedure.query(() => [{ id: '1', title: 'Hello' }]), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from '../server/router'; export const trpc = createTRPCClient({ links: [httpBatchLink({ url: 'YOUR_API_URL' })], }); // @filename: component.tsx // ---cut--- import { useQuery } from '@tanstack/react-query'; import { trpc } from './utils/trpc'; function PostList() { const { data } = useQuery({ queryKey: ['posts'] as const, queryFn: () => trpc.post.list.query(), }); data; // Post[] // ... } ```
## Setup ### 1. Install dependencies The following dependencies should be installed import { InstallSnippet } from '@site/src/components/InstallSnippet'; import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs'; :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: ### 2. Import your `AppRouter` ```twoslash include router // @filename: server/router.ts // ---cut--- import { initTRPC } from '@trpc/server'; import { z } from "zod"; const t = initTRPC.create(); const appRouter = t.router({ getUser: t.procedure.input(z.object({ id: z.string() })).query(() => ({ name: 'foo' })), createUser: t.procedure.input(z.object({ name: z.string() })).mutation(() => 'bar'), }); export type AppRouter = typeof appRouter; ``` ```twoslash include utils-a // @filename: utils/trpc.ts // ---cut--- import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from '../server/router'; export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext(); ``` import ImportAppRouter from '../../partials/_import-approuter.mdx'; ### 3a. Set up the tRPC context provider In cases where you rely on React context, such as when using server-side rendering in full-stack frameworks like Next.js, it's important to create a new QueryClient for each request so that your users don't end up sharing the same cache, you can use the `createTRPCContext` to create a set of type-safe context providers and consumers from your `AppRouter` type signature. ```tsx title='utils/trpc.ts' twoslash // @include: router // @include: utils-a ``` Then, create a tRPC client, and wrap your application in the `TRPCProvider`, as below. You will also need to set up and connect React Query, which [they document in more depth](https://tanstack.com/query/latest/docs/framework/react/quick-start). :::tip If you already use React Query in your application, you **should** re-use the `QueryClient` and `QueryClientProvider` you already have. You can read more about the QueryClient initialization in the [React Query docs](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#initial-setup). ::: ```tsx twoslash title='components/App.tsx' // @jsx: react-jsx // @include: router // @include: utils-a // @filename: components/App.tsx // ---cut--- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createTRPCClient, httpBatchLink } from '@trpc/client'; import { useState } from 'react'; import type { AppRouter } from '../server/router'; import { TRPCProvider } from '../utils/trpc'; function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }); } let browserQueryClient: QueryClient | undefined = undefined; function getQueryClient() { if (typeof window === 'undefined') { // Server: always make a new query client return makeQueryClient(); } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient(); return browserQueryClient; } } export function App() { const queryClient = getQueryClient(); const [trpcClient] = useState(() => createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:2022', }), ], }), ); return ( {null /* Your app here */} ); } ``` ### 3b. Set up with Query/Mutation Key Prefixing enabled If you want to prefix all queries and mutations with a specific key, see [Query Key Prefixing](./usage.mdx#keyPrefix) for setup and usage examples. ### 3c. Set up without React context When building an SPA using only client-side rendering with something like Vite, you can create the `QueryClient` and tRPC client outside of React context as singletons. ```ts twoslash title='utils/trpc.ts' // @include: router // @filename: utils/trpc.ts // ---cut--- import { QueryClient } from '@tanstack/react-query'; import { createTRPCClient, httpBatchLink } from '@trpc/client'; import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; import type { AppRouter } from '../server/router'; export const queryClient = new QueryClient(); const trpcClient = createTRPCClient({ links: [httpBatchLink({ url: 'http://localhost:2022' })], }); export const trpc = createTRPCOptionsProxy({ client: trpcClient, queryClient, }); ``` ```tsx twoslash title='components/App.tsx' // @include: router // @filename: utils/trpc.ts import { QueryClient } from '@tanstack/react-query'; import { createTRPCClient, httpBatchLink } from '@trpc/client'; import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; import type { AppRouter } from '../server/router'; export const queryClient = new QueryClient(); const trpcClient = createTRPCClient({ links: [httpBatchLink({ url: 'http://localhost:2022' })], }); export const trpc = createTRPCOptionsProxy({ client: trpcClient, queryClient, }); // @filename: components/App.tsx import React from 'react'; // ---cut--- import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from '../utils/trpc'; export function App() { return ( {/* Your app here */} ); } ``` ### 4. Fetch data You can now use the tRPC React Query integration to call queries and mutations on your API. ```tsx twoslash title='components/user-list.tsx' // @jsx: react-jsx // @include: router // @include: utils-a // @filename: components/user-list.tsx // ---cut--- import { useMutation, useQuery } from '@tanstack/react-query'; import { useTRPC } from '../utils/trpc'; export default function UserList() { const trpc = useTRPC(); // use `import { trpc } from './utils/trpc'` if you're using the singleton pattern const userQuery = useQuery(trpc.getUser.queryOptions({ id: 'id_bilbo' })); const userCreator = useMutation(trpc.createUser.mutationOptions()); return (

{userQuery.data?.name}

); } ``` -------------------------------------------------- --- id: usage title: TanStack React Query sidebar_label: Usage description: TanStack React Query usage slug: /client/tanstack-react-query/usage --- ## Quick example query ```tsx twoslash // @filename: server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ greeting: t.procedure.input(z.object({ name: z.string() })).query(({ input }) => `Hello ${input.name}` as const), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server/router'; export const { TRPCProvider, useTRPC } = createTRPCContext(); // @filename: component.tsx // ---cut--- import { useQuery } from '@tanstack/react-query'; import { useTRPC } from './trpc'; function Users() { const trpc = useTRPC(); const greetingQuery = useQuery(trpc.greeting.queryOptions({ name: 'Jerry' })); // greetingQuery.data === 'Hello Jerry' } ``` ## Usage The philosophy of this client is to provide thin and type-safe factories which work natively and type-safely with Tanstack React Query. This means just by following the autocompletes the client gives you, you can focus on building just with the knowledge the [TanStack React Query docs](https://tanstack.com/query/latest/docs/framework/react/overview) provide. ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ query: t.procedure.input(z.object({ id: z.string().optional() }).optional()).query(() => 'result'), mutation: t.procedure.mutation(() => 'ok'), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.tsx import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTRPC } from './trpc'; // ---cut--- export default function Basics() { const trpc = useTRPC(); const queryClient = useQueryClient(); // Create QueryOptions which can be passed to query hooks const myQueryOptions = trpc.path.to.query.queryOptions({ /** inputs */ }) const myQuery = useQuery(myQueryOptions) // or: // useSuspenseQuery(myQueryOptions) // useInfiniteQuery(myQueryOptions) // Create MutationOptions which can be passed to useMutation const myMutationOptions = trpc.path.to.mutation.mutationOptions() const myMutation = useMutation(myMutationOptions) // Create a QueryKey which can be used to manipulate many methods // on TanStack's QueryClient in a type-safe manner const myQueryKey = trpc.path.to.query.queryKey() const invalidateMyQueryKey = () => { queryClient.invalidateQueries({ queryKey: myQueryKey }) } return ( // Your app here null ) } ``` The `trpc` object is fully type-safe and will provide autocompletes for all the procedures in your `AppRouter`. At the end of the proxy, the following methods are available: ### `queryOptions` - querying data {#queryOptions} Available for all query procedures. Provides a type-safe wrapper around [Tanstack's `queryOptions` function](https://tanstack.com/query/latest/docs/framework/react/reference/queryOptions). The first argument is the input for the procedure, and the second argument accepts any native Tanstack React Query options. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ query: t.procedure.input(z.object({ id: z.string() })).query(() => 'result') }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const queryOptions = trpc.path.to.query.queryOptions( { /** input */ id: 'foo', }, { // Any Tanstack React Query options staleTime: 1000, }, ); // ---cut-after--- } ``` You can additionally provide a `trpc` object to the `queryOptions` function to provide tRPC request options to the client. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ query: t.procedure.input(z.object({ id: z.string() })).query(() => 'result') }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const queryOptions = trpc.path.to.query.queryOptions( { /** input */ id: 'foo', }, { trpc: { // Provide tRPC request options to the client context: { // see https://trpc.io/docs/client/links#managing-context }, }, }, ); // ---cut-after--- } ``` If you want to disable a query in a type safe way, you can use `skipToken`: ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ user: t.router({ details: t.procedure.input(z.object({ userId: z.string(), projectId: z.string() })).query(() => ({ name: 'foo' })), }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useQuery, skipToken } from '@tanstack/react-query'; import { useTRPC } from './trpc'; declare const user: { id: string } | undefined; declare const project: { id: string } | undefined; function Component() { const trpc = useTRPC(); // ---cut--- const query = useQuery( trpc.user.details.queryOptions( user?.id && project?.id ? { userId: user.id, projectId: project.id, } : skipToken, { staleTime: 1000, }, ), ); // ---cut-after--- } ``` The result can be passed to `useQuery` or `useSuspenseQuery` hooks or query client methods like `fetchQuery`, `prefetchQuery`, `prefetchInfiniteQuery`, `invalidateQueries`, etc. ### `infiniteQueryOptions` - querying infinite data {#infiniteQueryOptions} Available for all query procedures that take a cursor input. Provides a type-safe wrapper around [Tanstack's `infiniteQueryOptions` function](https://tanstack.com/query/latest/docs/framework/react/reference/infiniteQueryOptions). The first argument is the input for the procedure, and the second argument accepts any native Tanstack React Query options. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ query: t.procedure.input(z.object({ cursor: z.number().optional() })).query(() => ({ items: ['item'], nextCursor: 1 as number | undefined })), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const infiniteQueryOptions = trpc.path.to.query.infiniteQueryOptions( { /** input */ }, { // Any Tanstack React Query options getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }, ); // ---cut-after--- } ``` ### `queryKey` - getting the query key and performing operations on the query client {#queryKey} Available for all query procedures. Allows you to access the query key in a type-safe manner. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ query: t.procedure.input(z.object({ id: z.string() })).query(() => 'result') }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const queryKey = trpc.path.to.query.queryKey(); // ---cut-after--- } ``` Since Tanstack React Query uses fuzzy matching for query keys, you can also create a partial query key for any sub-path to match all queries belonging to a router: ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const appRouter = t.router({ router: t.router({ someQuery: t.procedure.query(() => 'result') }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const queryKey = trpc.router.pathKey(); // ---cut-after--- } ``` Or even the root path to match all tRPC queries: ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const appRouter = t.router({ someQuery: t.procedure.query(() => 'result') }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const queryKey = trpc.pathKey(); // ---cut-after--- } ``` ### `infiniteQueryKey` - getting the infinite query key {#infiniteQueryKey} Available for all query procedures that take a cursor input. Allows you to access the query key for an infinite query in a type-safe manner. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ query: t.procedure.input(z.object({ cursor: z.number().optional() })).query(() => ({ items: ['item'], nextCursor: 1 as number | undefined })), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const infiniteQueryKey = trpc.path.to.query.infiniteQueryKey({ /** input */ }); // ---cut-after--- } ``` The result can be used with query client methods like `getQueryData`, `setQueryData`, `invalidateQueries`, etc. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ query: t.procedure.input(z.object({ cursor: z.number().optional() })).query(() => ({ items: ['item'], nextCursor: 1 as number | undefined })), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useQueryClient } from '@tanstack/react-query'; import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); const queryClient = useQueryClient(); // ---cut--- // Get cached data for an infinite query const cachedData = queryClient.getQueryData( trpc.path.to.query.infiniteQueryKey({ cursor: 0 }), ); // Set cached data for an infinite query queryClient.setQueryData( trpc.path.to.query.infiniteQueryKey({ cursor: 0 }), (data) => { // Modify the data return data; }, ); // ---cut-after--- } ``` ### `queryFilter` - creating query filters {#queryFilter} Available for all query procedures. Allows creating [query filters](https://tanstack.com/query/latest/docs/framework/react/guides/filters#query-filters) in a type-safe manner. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ query: t.procedure.input(z.object({ id: z.string() })).query(() => 'result') }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const queryFilter = trpc.path.to.query.queryFilter( { /** input */ }, { // Any Tanstack React Query filter predicate: (query) => { return !!query.state.data; }, }, ); // ---cut-after--- } ``` Like with query keys, if you want to run a filter across a whole router you can use `pathFilter` to target any sub-path. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ someQuery: t.procedure.query(() => 'result') }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const queryFilter = trpc.path.pathFilter({ // Any Tanstack React Query filter predicate: (query) => { return !!query.state.data; }, }); // ---cut-after--- } ``` Useful for creating filters that can be passed to client methods like `queryClient.invalidateQueries` etc. ### `infiniteQueryFilter` - creating infinite query filters {#infiniteQueryFilter} Available for all query procedures that take a cursor input. Allows creating [query filters](https://tanstack.com/query/latest/docs/framework/react/guides/filters#query-filters) for infinite queries in a type-safe manner. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ query: t.procedure.input(z.object({ cursor: z.number().optional() })).query(() => ({ items: ['item'], nextCursor: 1 as number | undefined })), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const infiniteQueryFilter = trpc.path.to.query.infiniteQueryFilter( { /** input */ }, { // Any Tanstack React Query filter predicate: (query) => { return !!query.state.data; }, }, ); // ---cut-after--- } ``` Useful for creating filters that can be passed to client methods like `queryClient.invalidateQueries` etc. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ query: t.procedure.input(z.object({ cursor: z.number().optional() })).query(() => ({ items: ['item'], nextCursor: 1 as number | undefined })), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useQueryClient } from '@tanstack/react-query'; import { useTRPC } from './trpc'; async function Component() { const trpc = useTRPC(); const queryClient = useQueryClient(); // ---cut--- await queryClient.invalidateQueries( trpc.path.to.query.infiniteQueryFilter( {}, { predicate: (query) => { // Filter logic based on query state return query.state.status === 'success'; }, }, ), ); // ---cut-after--- } ``` ### `mutationOptions` - creating mutation options {#mutationOptions} Available for all mutation procedures. Provides a type-safe identity function for constructing options that can be passed to `useMutation`. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ mutation: t.procedure.input(z.object({ id: z.string() })).mutation(() => 'ok' as const), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const mutationOptions = trpc.path.to.mutation.mutationOptions({ // Any Tanstack React Query options onSuccess: (data) => { // do something with the data }, }); // ---cut-after--- } ``` ### `mutationKey` - getting the mutation key {#mutationKey} Available for all mutation procedures. Allows you to get the mutation key in a type-safe manner. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ mutation: t.procedure.input(z.object({ id: z.string() })).mutation(() => 'ok' as const), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; function Component() { const trpc = useTRPC(); // ---cut--- const mutationKey = trpc.path.to.mutation.mutationKey(); // ---cut-after--- } ``` ### `subscriptionOptions` - creating subscription options {#subscriptionOptions} TanStack does not provide a subscription hook, so we continue to expose our own abstraction here which works with a [standard tRPC subscription setup](/docs/server/subscriptions). Available for all subscription procedures. Provides a type-safe identity function for constructing options that can be passed to `useSubscription`. Note that you need to have either the [`httpSubscriptionLink`](/docs/client/links/httpSubscriptionLink) or [`wsLink`](/docs/client/links/wsLink) configured in your tRPC client to use subscriptions. ```tsx twoslash // @jsx: react-jsx // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ subscription: t.procedure.input(z.object({ channel: z.string().optional() }).optional()).subscription(async function* () { yield 'data' as string; }), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.tsx import { useTRPC } from './trpc'; import { useSubscription } from '@trpc/tanstack-react-query'; // ---cut--- function SubscriptionExample() { const trpc = useTRPC(); const subscription = useSubscription( trpc.path.to.subscription.subscriptionOptions( { /** input */ }, { enabled: true, onStarted: () => { // do something when the subscription is started }, onData: (data) => { // you can handle the data here }, onError: (error) => { // you can handle the error here }, onConnectionStateChange: (state) => { // you can handle the connection state here }, }, ), ); // Or you can handle the state here subscription.data; // The lastly received data subscription.error; // The lastly received error /** * The current status of the subscription. * Will be one of: `'idle'`, `'connecting'`, `'pending'`, or `'error'`. * * - `idle`: subscription is disabled or ended * - `connecting`: trying to establish a connection * - `pending`: connected to the server, receiving data * - `error`: an error occurred and the subscription is stopped */ subscription.status; // Reset the subscription (if you have an error etc) subscription.reset(); return <>{/* ... */}; } ``` ### Query Key Prefixing {#keyPrefix} When using multiple tRPC providers in a single application (e.g., connecting to different backend services), queries with the same path will collide in the cache. You can prevent this by enabling query key prefixing. ```tsx twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const authRouter = t.router({ list: t.procedure.query(() => [{ id: '1' }]) }); const billingRouter = t.router({ list: t.procedure.query(() => [{ id: '1' }]) }); export type AuthRouter = typeof authRouter; export type BillingRouter = typeof billingRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AuthRouter, BillingRouter } from './server'; const auth = createTRPCContext(); const billing = createTRPCContext(); export const useTRPCAuth = auth.useTRPC; export const useTRPCBilling = billing.useTRPC; // @filename: component.ts import { useQuery } from '@tanstack/react-query'; import { useTRPCAuth, useTRPCBilling } from './trpc'; function Component() { const trpcAuth = useTRPCAuth(); const trpcBilling = useTRPCBilling(); // ---cut--- // Without prefixes - these would collide! const authQuery = useQuery(trpcAuth.list.queryOptions()); // auth service const billingQuery = useQuery(trpcBilling.list.queryOptions()); // billing service // ---cut-after--- } ``` Enable the feature flag when creating your context: ```tsx twoslash title='utils/trpc.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const billingRouter = t.router({ list: t.procedure.query(() => [{ id: '1' }]) }); const accountRouter = t.router({ list: t.procedure.query(() => [{ id: '1' }]) }); export type BillingRouter = typeof billingRouter; export type AccountRouter = typeof accountRouter; // @filename: utils/trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import { createTRPCClient } from '@trpc/client'; import type { BillingRouter, AccountRouter } from '../server'; // ---cut--- // [...] const billing = createTRPCContext(); export const BillingProvider = billing.TRPCProvider; export const useBilling = billing.useTRPC; export const createBillingClient = () => createTRPCClient({ links: [ /* ... */ ], }); const account = createTRPCContext(); export const AccountProvider = account.TRPCProvider; export const useAccount = account.useTRPC; export const createAccountClient = () => createTRPCClient({ links: [ /* ... */ ], }); ``` ```tsx twoslash title='App.tsx' // @jsx: react-jsx // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const billingRouter = t.router({ list: t.procedure.query(() => [{ id: '1' }]) }); const accountRouter = t.router({ list: t.procedure.query(() => [{ id: '1' }]) }); export type BillingRouter = typeof billingRouter; export type AccountRouter = typeof accountRouter; // @filename: utils/trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import { createTRPCClient } from '@trpc/client'; import type { BillingRouter, AccountRouter } from '../server'; const billing = createTRPCContext(); export const BillingProvider = billing.TRPCProvider; export const createBillingClient = () => createTRPCClient({ links: [] }); const account = createTRPCContext(); export const AccountProvider = account.TRPCProvider; export const createAccountClient = () => createTRPCClient({ links: [] }); // @filename: App.tsx // ---cut--- import { useState } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { BillingProvider, AccountProvider, createBillingClient, createAccountClient, } from './utils/trpc'; // [...] export function App() { const [queryClient] = useState(() => new QueryClient()); const [billingClient] = useState(() => createBillingClient()); const [accountClient] = useState(() => createAccountClient()); return (
{/* ... */}
); } ``` ```tsx twoslash title='components/MyComponent.tsx' // @jsx: react-jsx // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const billingRouter = t.router({ list: t.procedure.query(() => [{ id: '1' }]) }); const accountRouter = t.router({ list: t.procedure.query(() => [{ id: '1' }]) }); export type BillingRouter = typeof billingRouter; export type AccountRouter = typeof accountRouter; // @filename: utils/trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { BillingRouter, AccountRouter } from '../server'; const billing = createTRPCContext(); export const useBilling = billing.useTRPC; const account = createTRPCContext(); export const useAccount = account.useTRPC; // @filename: components/MyComponent.tsx // ---cut--- import { useQuery } from '@tanstack/react-query'; import { useBilling, useAccount } from '../utils/trpc'; // [...] export function MyComponent() { const billing = useBilling(); const account = useAccount(); const billingList = useQuery(billing.list.queryOptions()); const accountList = useQuery(account.list.queryOptions()); return (
Billing: {JSON.stringify(billingList.data ?? null)}
Account: {JSON.stringify(accountList.data ?? null)}
); } ``` The query keys will be properly prefixed to avoid collisions: ```tsx twoslash // Example of how the query keys look with prefixes const queryKeys = [ [['billing'], ['list'], { type: 'query' }], [['account'], ['list'], { type: 'query' }], ]; ``` ### Inferring Input and Output types When you need to infer the input and output types for a procedure or router, there are 2 options available depending on the situation. Infer the input and output types of a full router ```ts twoslash // @filename: server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ getUser: t.procedure.input(z.object({ id: z.string() })).query(() => ({ name: 'foo' })), }); export type AppRouter = typeof appRouter; // @filename: types.ts // ---cut--- import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; import type { AppRouter } from './server/router'; export type Inputs = inferRouterInputs; export type Outputs = inferRouterOutputs; ``` Infer types for a single procedure ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ procedure: t.procedure.input(z.object({ id: z.string() })).query(() => ({ name: 'foo' })), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server'; export const { useTRPC } = createTRPCContext(); // @filename: component.ts import { useTRPC } from './trpc'; // ---cut--- import type { inferInput, inferOutput } from '@trpc/tanstack-react-query'; function Component() { const trpc = useTRPC(); type Input = inferInput; type Output = inferOutput; } ``` ### Accessing the tRPC client {#useTRPCClient} If you used the [setup with React Context](/docs/client/tanstack-react-query/setup#3a-set-up-the-trpc-context-provider), you can access the tRPC client using the `useTRPCClient` hook. ```tsx twoslash // @filename: server/router.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ getUser: t.procedure.input(z.object({ id: z.string() })).query(() => ({ name: 'foo' })), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCContext } from '@trpc/tanstack-react-query'; import type { AppRouter } from './server/router'; export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext(); // @filename: component.tsx // ---cut--- import { useTRPCClient } from './trpc'; async function Component() { const trpcClient = useTRPCClient(); const result = await trpcClient.getUser.query({ id: '1', }); } ``` If you [setup without React Context](/docs/client/tanstack-react-query/setup#3c-set-up-without-react-context), you can import the global client instance directly instead. ```ts twoslash // @module: esnext // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ path: t.router({ to: t.router({ procedure: t.procedure.input(z.object({ id: z.string() })).query(() => ({ name: 'foo' })), }) }), }); export type AppRouter = typeof appRouter; // @filename: trpc.ts import { createTRPCClient } from '@trpc/client'; import type { AppRouter } from './server'; export const client = createTRPCClient({ links: [] }); // @filename: example.ts // ---cut--- import { client } from './trpc'; const result = await client.path.to.procedure.query({ /** input */ id: 'foo', }); ``` -------------------------------------------------- --- id: aborting-procedure-calls title: Aborting Procedure Calls sidebar_label: Aborting Procedure Calls slug: /client/vanilla/aborting-procedure-calls --- tRPC supports the standard `AbortController`/`AbortSignal` API for aborting procedures. All you have to do is pass an `AbortSignal` to the query or mutation options, and call the `AbortController` instance's `abort` method if you need to cancel the request. ```ts twoslash title="utils.ts" // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ userById: t.procedure.input(z.string()).query(({ input }) => ({ id: input, name: 'Bilbo' })), }); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000/trpc', }), ], }); // 1. Create an AbortController instance - this is a standard javascript API const ac = new AbortController(); // 2. Pass the signal to a query or mutation const query = client.userById.query('id_bilbo', { signal: ac.signal }); // 3. Cancel the request if needed ac.abort(); ``` -------------------------------------------------- --- id: infer-types title: Inferring Types sidebar_label: Inferring Types slug: /client/vanilla/infer-types --- ```twoslash include server // @module: esnext // @filename: server.ts // ---cut--- import { initTRPC } from '@trpc/server'; import { z } from "zod"; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ list: t.procedure .query(() => { // imaginary db call return [{ id: 1, title: 'tRPC is the best!' }]; }), byId: t.procedure .input(z.string()) .query((opts) => { // imaginary db call return { id: 1, title: 'tRPC is the best!' }; }), create: t.procedure .input(z.object({ title: z.string(), text: z.string(), })) .mutation((opts) => { // imaginary db call return { id: 1, ...opts.input }; }), onPostAdd: t.procedure .input(z.object({ authorId: z.string() })) .subscription(async function* ({ input }) { // imaginary event source yield { id: 1, title: 'tRPC is the best!', authorId: input.authorId, }; }), }), }); export type AppRouter = typeof appRouter; ``` It is often useful to access the types of your API within your clients. For this purpose, you are able to infer the types contained in your `AppRouter`. `@trpc/server` exports the following helper types to assist with inferring these types from the `AppRouter` exported by your `@trpc/server` router: - `inferRouterInputs` - `inferRouterOutputs` - `inferProcedureInput` - `inferProcedureOutput` - `inferSubscriptionInput` - `inferSubscriptionOutput` ## Inferring Input & Output Types Let's assume we have this example router: ```ts twoslash title='server.ts' // @include: server ``` Using the helpers, we can infer the types of our router. The following example shows how to infer the types of the `post.create` procedure: ```ts twoslash title="client.ts" // @module: esnext // @include: server // @filename: client.ts // ---cut--- import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; import type { AppRouter } from './server'; type RouterInput = inferRouterInputs; type RouterOutput = inferRouterOutputs; type PostCreateInput = RouterInput['post']['create']; // ^? type PostCreateOutput = RouterOutput['post']['create']; // ^? ``` ## Inferring Individual Procedure Types If you already have access to a specific procedure on your router, you can infer its input or output directly: ```ts twoslash title="client.ts" // @module: esnext // @include: server // @filename: client.ts // ---cut--- import type { inferProcedureInput, inferProcedureOutput, } from '@trpc/server'; import type { AppRouter } from './server'; type PostByIdInput = inferProcedureInput; // ^? type PostByIdOutput = inferProcedureOutput; // ^? ``` For subscriptions, you can infer the subscription input and the emitted data type: ```ts twoslash title="client.ts" // @module: esnext // @include: server // @filename: client.ts // ---cut--- import type { inferSubscriptionInput, inferSubscriptionOutput, } from '@trpc/server'; import type { AppRouter } from './server'; type OnPostAddInput = inferSubscriptionInput; // ^? type OnPostAddOutput = inferSubscriptionOutput; // ^? ``` ## Infer `TRPCClientError` types It's also useful to infer the error type for your `AppRouter` ```ts twoslash title='client.ts' // @module: esnext // @include: server // @filename: trpc.ts import { createTRPCClient, httpBatchLink } from "@trpc/client"; import type { AppRouter } from "./server"; export const trpc = createTRPCClient({ links: [ httpBatchLink({ url: "http://localhost:3000/api/trpc", }), ], }); // @filename: client.ts // ---cut--- import { TRPCClientError } from '@trpc/client'; import type { AppRouter } from './server'; import { trpc } from './trpc'; export function isTRPCClientError( cause: unknown, ): cause is TRPCClientError { return cause instanceof TRPCClientError; } async function main() { try { await trpc.post.byId.query('1'); } catch (cause) { if (isTRPCClientError(cause)) { // `cause` is now typed as your router's `TRPCClientError` console.log('data', cause.data); // ^? } else { // [...] } } } main(); ``` -------------------------------------------------- --- id: overview title: tRPC Client sidebar_label: Overview slug: /client/vanilla --- # tRPC Client The "Vanilla" tRPC client can be used to call your API procedures as if they are local functions, enabling a seamless development experience. ```ts twoslash // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ getUser: t.procedure.input(z.string()).query(({ input }) => ({ id: input, name: 'Bilbo' })), }); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [httpBatchLink({ url: 'http://localhost:3000' })], }); const bilbo = await client.getUser.query('id_bilbo'); // => { id: 'id_bilbo', name: 'Bilbo' }; ``` ### When to use the Vanilla Client? You are likely to use this client in two scenarios: - With a frontend framework for which we don't have an official integration - With a separate backend service written in TypeScript. ### When **NOT** to use the Vanilla Client? - While you _can_ use the client to call procedures from a React component, you should usually use our [TanStack React Query Integration](../tanstack-react-query/setup.mdx). It offers many additional features such as the ability to manage loading and error state, caching, and invalidation. - We recommend you do not use this client when calling procedures of the same API instance, this is because the invocation has to pass through the network layer. For complete recommendations on invoking a procedure in the current API, you can [read more here](/docs/server/server-side-calls). -------------------------------------------------- --- id: setup title: Set up a tRPC Client sidebar_label: Setup slug: /client/vanilla/setup --- import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs'; ### 1. Install the tRPC Client library Use your preferred package manager to install the `@trpc/client` library, and also install `@trpc/server` which contains some required types. import { InstallSnippet } from '@site/src/components/InstallSnippet'; :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: ### 2. Import your App Router import ImportAppRouter from '../../partials/_import-approuter.mdx'; ### 3. Initialize the tRPC client Create a tRPC client with the `createTRPCClient` method, and add a `links` array with a [terminating link](../links/overview.md#the-terminating-link) pointing at your API. To learn more about tRPC links, [click here](../links/overview.md). ```ts twoslash title='client.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts declare function getAuthCookie(): string; // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000/trpc', // You can pass any HTTP headers you wish here async headers() { return { authorization: getAuthCookie(), }; }, }), ], }); ``` ### 4. Use the tRPC Client Under the hood this creates a typed [JavaScript Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) which allows you to interact with your tRPC API in a fully type-safe way: ```ts twoslash title='client.ts' // @target: esnext // @filename: server.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ getUser: t.procedure.input(z.string()).query(({ input }) => ({ id: input, name: 'Bilbo' })), createUser: t.procedure.input(z.object({ name: z.string() })).mutation(({ input }) => ({ id: 'id_frodo', ...input })), }); export type AppRouter = typeof appRouter; // @filename: client.ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient({ links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })], }); // ---cut--- const bilbo = await client.getUser.query('id_bilbo'); // => { id: 'id_bilbo', name: 'Bilbo' }; const frodo = await client.createUser.mutate({ name: 'Frodo' }); // => { id: 'id_frodo', name: 'Frodo' }; ``` You're all set! -------------------------------------------------- --- id: awesome-trpc title: Awesome tRPC Collection sidebar_label: Awesome tRPC Collection slug: /community/awesome-trpc --- A collection of resources on tRPC. **Please** edit this page and add your own links! 🙏 ## 🧩 Extensions & community add-ons ### Extensions | Description | Link | | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | tRPC Docs Generator - Auto generate interactive documentation for your tRPC APIs | https://github.com/liorcodev/trpc-docs-generator | | tRPC-ui - Automatically generates a UI for manually testing your tRPC backend | https://github.com/aidansunbury/trpc-ui | | trpc-to-openapi - OpenAPI & REST support for your tRPC routers | https://github.com/mcampa/trpc-to-openapi | | tRPC Client Devtools browser extension | https://github.com/rhenriquez28/trpc-client-devtools | | tRPC Playground - sandbox for testing tRPC queries in the browser | https://github.com/sachinraja/trpc-playground | | tRPC-Chrome - Web extensions messaging support for tRPC | https://github.com/jlalmes/trpc-chrome | | Step CI - Automated API Testing and Quality Assurance | https://github.com/stepci/stepci | | msw-trpc - tRPC support for MSW | https://github.com/maloguertin/msw-trpc | | trpc-cli - Turn a tRPC router into a type-safe, fully documented CLI | https://github.com/mmkal/trpc-cli | | trpc-live - Live query solution for tRPC | https://github.com/strblr/trpc-live | | trpc-navigation-plugin - Fix goto-definition with emit declarations | https://github.com/ebg1223/trpc-navigation-plugin | | oRPC - OpenAPI support for your tRPC routers | https://orpc.unnoq.com/docs/openapi/integrations/trpc | | Prisma tRPC Generator - Automatically generate tRPC routers from Prisma schemas | https://github.com/omar-dulaimi/prisma-trpc-generator | | tRPC Shield - Permission system for tRPC | https://github.com/omar-dulaimi/trpc-shield | | Prisma tRPC Shield Generator - Generate tRPC Shield permissions from Prisma schema | https://github.com/omar-dulaimi/prisma-trpc-shield-generator | | trpc-to-mcp - Turn a tRPC router into MCP tools, server and handler | https://github.com/iboughtbed/trpc-to-mcp | | tRPC Studio - Swagger-like UI with input forms, Try It Out, output type visualization, and CLI extractor | https://github.com/sayefdeen/trpc-studio | ### Frontend frameworks | Description | Link | | ------------------------------------------------------- | ----------------------------------------------- | | tRPC-SvelteKit - SvelteKit tRPC extension | https://github.com/icflorescu/trpc-sveltekit | | tRPC-Remix - Adapter for Remix | https://github.com/ggrandi/trpc-remix | | tRPC-Remix-Call - Client and server side call for Remix | https://github.com/simonboisset/trpc-remix-call | | tRPC Client For SolidJS W/ Solid Query | https://github.com/OrJDev/solid-trpc | | tRPC API Handler For SolidStart | https://github.com/OrJDev/solid-start-trpc | | tRPC-nuxt - Nuxt 3 module | https://github.com/wobsoriano/trpc-nuxt | ### Bootstrappers | Description | Link | | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | create-t3-app - Scaffold a starter project using the T3 Stack (Next.js, tRPC, Tailwind CSS, Prisma) | https://create.t3.gg | | sidebase - Scaffold a starter project using sidebase (Nuxt 3, tRPC, Tailwind CSS, Prisma) | https://sidebase.io | | Create JD App - Scaffold a starter project using the JD Stack (SolidStart, tRPC, Tailwind, Prisma) | https://github.com/OrJDev/create-jd-app | | Create tRPC App - Create tRPC-powered apps with one command | https://github.com/omar-dulaimi/create-trpc-app | | viteRPC - Monorepo template powered by Vite (Vite, tRPC, Tailwind CSS) | https://github.com/mnik01/viteRPC | | Start UI [web] - Opinionated frontend starter (tRPC, Prisma, Next.js, Chakra UI) | https://github.com/bearstudio/start-ui-web | | Nx Plugin for AWS - Scaffold a project using tRPC with Nx, and deploy to AWS with CDK or Terraform | https://awslabs.github.io/nx-plugin-for-aws/en/guides/trpc/ | ### Library adapters | Description | Link | | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | tRPC-uWebSockets - Adapter for uWebSockets.js server | https://github.com/romanzy-1612/trpc-uwebsockets | | jotai-trpc - Jotai wrapper around tRPC vanilla client | https://github.com/jotai-labs/jotai-trpc | | trpc-zustand - Zustand wrapper around tRPC client | https://github.com/strblr/trpc-zustand | | @h4ad/serverless-adapter - Connect tRPC with AWS SQS, AWS API Gateway, and many more event sources. | https://viniciusl.com.br/serverless-adapter/docs/main/frameworks/trpc | | trpc-koa-adapter - tRPC adapter for Koa server | https://github.com/BlairCurrey/trpc-koa-adapter | | tRPC - iron-session | https://github.com/parkgang/trpc-iron-session | | electron-trpc - Electron support for tRPC | https://github.com/jsonnull/electron-trpc | | cloudflare-pages-plugin-trpc - Quickly create a tRPC server with a Cloudflare Pages Function | https://github.com/toyamarinyon/cloudflare-pages-plugin-trpc | | ZenStack - Full-stack toolkit adds access control to Prisma and generates trpc routers from schema | https://github.com/zenstackhq/zenstack | | tRPC-SWR - tRPC adapter for Vercel's SWR client | https://trpc-swr.vercel.app/ | | trpc-rtk-query - Automatically generate RTK Query api endpoints from your tRPC setup | https://github.com/otahontas/trpc-rtk-query | | k6-trpc - k6 compatible tRPC client | https://github.com/dextertanyj/k6-trpc | | trpc-token-refresh-link - Link to refresh access tokens and refresh tokens | https://github.com/larskarbo/trpc-token-refresh-link | | trpc-bun-adapter - tRPC adapter for Bun runtime environment | https://github.com/cah4a/trpc-bun-adapter | | trpc-rabbitmq - tRPC adapter using RabbitMQ as a transport layer | https://github.com/imxeno/trpc-rabbitmq | | trpc-mqtt - tRPC adapter using MQTT as a transport layer | https://github.com/edorgeville/trpc-mqtt | | trpc-redis - tRPC adapter using Redis as a transport layer | https://github.com/cobeo2004/trpc-redis | | NestJS-tRPC - An opinionated approach to building end-to-end typesafe APIs with tRPC within NestJS. | https://www.nestjs-trpc.io/ | ## 🍀 Starting points, example projects, etc | Description | Link | | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | **Recommended:** Starter project with Prisma, Next.js, tRPC, E2E-testing | https://github.com/trpc/examples-next-prisma-starter | | **create-t3-turbo** - Clean and simple starter repo using the T3 Stack along with Expo React Native | https://github.com/t3-oss/create-t3-turbo | | Subscriptions Starter Project using SSE | https://github.com/trpc/examples-next-sse-chat | | WebSockets Starter Project | https://github.com/trpc/examples-next-prisma-starter-websockets | | tRPC Kitchen Sink - A collection of tRPC usage patterns. | https://github.com/trpc/examples-kitchen-sink | | Turborepo + Expo + tRPC Starter | https://github.com/gunnnnii/turbo-expo-trpc-starter | | tRPC-SvelteKit Example Application | https://github.com/icflorescu/trpc-sveltekit-example | | tRPC + Ultra | https://github.com/sachinraja/trpc-ultra | | Nx Monorepo + tRPC + Prisma | https://github.com/nowlena/nx-trpc-test | | tRPC (w/ Fetch Adapter) + SvelteKit + Tailwind CSS | https://github.com/austins/trpc-sveltekit-fetchadapter-example | | Sign-In With Ethereum tRPC + ViteJS React | https://github.com/codingwithmanny/trpc-siwe-monorepo | | tRPC + Vue3 todo example project (tRPC, Express.js, Vue3, Prisma, vue-query) | https://github.com/guushamann/Todo-tRPC-Vue3 | | Vite + Svelte + tRPC | https://github.com/mishankov/vite-svelte-trpc | | V3 - A T3 Inspired Nuxt Stack | https://github.com/CRBroughton/V3 | | Twitter clone - A simple Twitter clone built with T3 Stack + NextAuth + Supabase + Prisma | https://github.com/AlandSleman/t3-twitter-clone | | Separate backend & frontend repositories | https://github.com/mkosir/trpc-api-boilerplate | | tRPC + Deno + fresh | https://github.com/LunaTK/fresh-trpc-example | | Ethereum Decentralized Application example (Next.js, tRPC, Ethers, Hardhat, Solidity) | https://github.com/tr1sm0s1n/next-trpc-dapp | | Yarn Workspaces + ESLint + CSpell + Commitlint + Lefthook + tRPC + React Query + Vite | https://github.com/sotnikov-link/yarn-workspaces-eslint-cspell-commitlint-lefthook-template | | Fullstack SaaS Boilerplate: Trpc Fastify React | https://github.com/alan345/Fullstack-SaaS-Boilerplate | | tRPC realtime server with Fastify, Prisma, Redis (or type-safe event emitter) and PostgreSQL | https://github.com/cobeo2004/trpc-fastify | | Build a Public tRPC API: trpc-openapi vs ts-rest | https://catalins.tech/public-api-trpc/ | | Next.js + Expo + tRPC Monorepo Boilerplate | https://github.com/monsieursam/boilerplate-next-expo-trpc | | Next.js + Express + BetterAuth + Prisma + bun + shadcn + tRPC Monorepo Boilerplate | https://github.com/KitsuneKode/template-nextjs-express-trpc-better-auth-monorepo | ## 🏁 Open-source projects using tRPC | Description | Link | | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | [Cal.com](https://cal.com) - Scheduling infrastructure | https://github.com/calcom/cal.com | | [Skill Recordings](https://github.com/skillrecordings/products) - Hosting courses by Matt Pocock, Kent C Dodds, Dan Abramov, and many others | https://github.com/skillrecordings/products | | [SST](https://sst.dev) - A framework that makes it easy to build serverless apps. | https://github.com/serverless-stack/sst | | [Beam](https://planetscale.com/blog/introducing-beam) - A simple message board for your organization or project. | https://github.com/planetscale/beam | | [Rallly](https://rallly.co) - Self-hostable doodle poll alternative. | https://github.com/lukevella/rallly | | [Hilde](http://hilde.gg) - Match-making app for games like foosball, air hockey and similar | https://github.com/nehalist/hilde | | [Answer Overflow](https://www.answeroverflow.com/) - Discord bot that indexes help channel content into Google | https://github.com/AnswerOverflow/AnswerOverflow | | [Prisma Editor](https://prisma-editor.bahumaish.app/) - Powerful tool to visualize and edit Prisma Schema | https://github.com/mohammed-bahumaish/prisma-editor | | [Saleor Apps](https://saleor.io/) - Official apps/integrations for Saleor Commerce | https://github.com/saleor/apps | | [Rao Pics App](https://github.com/rao-pics/rao-pics) - Visit Photo on any device. Supported MacOS/Windows | https://github.com/rao-pics/rao-pics | | [Tianji](https://tianji.msgbyte.com/) - All-in-One Insight Hub. Tianji = Website Analytics + Uptime Monitor + Server Status | https://github.com/msgbyte/tianji | | [Workplacify](https://workplacify.com/) - Desk scheduling software, for hybrid and in-office teams | https://workplacify.com/ | | [Dotfyle](https://dotfyle.com/) - Discover and share Neovim plugins | https://dotfyle.com/ | | [ConvoForm](https://www.convoform.com/?utm_source=github&utm_medium=social&utm_campaign=trpc) - Create your own AI-Powered conversational form | https://www.convoform.com/ | -------------------------------------------------- --- id: contributing title: Contributing sidebar_label: Contributing slug: /community/contributing --- import Content from '@site/unversioned/_contributing.mdx'; -------------------------------------------------- --- id: love title: Testimonials / Love sidebar_label: Testimonials slug: /community/love --- import Content from '@site/unversioned/_love.mdx'; -------------------------------------------------- --- id: sponsors title: Sponsors sidebar_label: Sponsors slug: /community/sponsors --- import Content from '@site/unversioned/_sponsors.mdx'; -------------------------------------------------- --- id: faq title: FAQ / Troubleshooting sidebar_label: FAQ / Troubleshooting slug: /faq --- Collection of frequently asked questions with ideas on how to troubleshoot & resolve them. Feel free to contribute to this page with improvements or create a new discussion [on GitHub](https://github.com/trpc/trpc/discussions) if you have a question that isn't answered here. Also, have a look through the [GitHub Discussions](https://github.com/trpc/trpc/discussions) and our [Discord](https://trpc.io/discord) if your question isn't answered here. ## It doesn't work! I'm getting `any` everywhere - Make sure you have no type errors in your code - Make sure you have `"strict": true` in your `tsconfig.json` - Make sure your `@trpc/*`-versions match in your `package.json` - Make sure you are using the TypeScript-version required by tRPC (`>=5.7.2`) - Make sure your editor is using the same TypeScript version as your `package.json` ### VSCode settings Add these settings to your `.vscode/settings.json` in your project root to make sure your editor is using the same TypeScript version as your `package.json`: ```json title=".vscode/settings.json" { "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true } ``` We highly recommend committing this file to your repo so your colleagues also get the same experience. ## How do I make a middleware change the type of my `Context`? See [Context Extension](/docs/server/middlewares#context-extension). ## Is tRPC production ready? Yes. tRPC is very stable and is used by thousands of companies, even big ones like [Netflix](https://netflix.com) & [Pleo](https://pleo.io) are using tRPC in production. ## Why doesn't tRPC work in my monorepo? This is a difficult question to answer, but since tRPC doesn't have any build step, it's unlikely that the problem is on tRPC's side. Here are some things to check: - Make sure you have the same version of all `@trpc/*` across all your projects - Make sure you have `"strict": true` in all your `tsconfig.json`s - Make sure you have no type errors in your app - In the case that you have a dedicated server and client `tsconfig.json` files without a bundled server monorepo package, make sure you have `"paths": [...]` in your client `tsconfig.json` like your server `tsconfig.json`, so that the client can find the same file. You can also have a look at our [Awesome tRPC](/docs/community/awesome-trpc)-collection to find several open-source projects that are using tRPC in a monorepo. ## Is a monorepo mandatory? No, a monorepo is not mandatory but you will lose some of the benefits of using tRPC if you don't use it since you will lose guarantees that your client and server works together. One way you can leverage tRPC is to publish a private npm package with the types of your backend repo and consume them in your frontend repo. > _Related discussion: https://github.com/trpc/trpc/discussions/1860_ ## Can I dynamically return a different output depending on what input I send? No, not currently, in order for tRPC to do that automatically, we need something called "Higher kinded types" which is not yet supported in TypeScript. > _Related discussion: https://github.com/trpc/trpc/discussions/2150_ ## Can I apply a middleware to a full router? No, but you can use [base procedures](/docs/server/procedures#reusable-base-procedures) instead, which offers more flexibility than if this was done on a per-router-level. ## Does tRPC work with Next.js App Router & RSC? Yes, tRPC works with Next.js App Router & React Server Components. See the [Next.js App Router setup guide](/docs/client/nextjs/app-router-setup) for the recommended approach. ## Am I safe with using features marked as `unstable_`? {#unstable} **tl;dr**: Yes! If you encounter a feature in tRPC that is marked as `unstable_` it means that the API is unstable and might change in minor version bumps, but: - Specifics of the implementation might change in minor changes - its name and options might change - If it exists in tRPC it's already being used in production - We very much encourage you to use it - If any changes are done to `unstable_`-feature, they will be included in the release notes (& you'll see type errors) - Please report any suggestion on the API design or issues on [github.com/trpc/trpc/issues](https://github.com/trpc/trpc/issues) or in the `#🧪-unstable-experimental-features` on [our Discord](https://trpc.io/discord) ## Am I safe with using features marked as `experimental_`? {#experimental} If you encounter a feature in tRPC that is marked as `experimental_` it means that the API is unstable and is very likely to change during any bump of tRPC. - Wide range of the feature and its usage might change - The feature might not be well-tested - We might drop the feature entirely - It's up to you to read the latest docs and upgrade without a guided migration path - Changes might not be well-documented in the release notes - Bugs are not guaranteed to be fixed We do, however, love input! Please report any suggestion on the API design or issues in the `#🧪-unstable-experimental-features` on [our Discord](https://trpc.io/discord). ## Is tRPC strict with semver? {#semver} Yes, tRPC is very strict with [semantic versioning](https://semver.org/) and we will never introduce breaking changes in a minor version bump. With this, we also consider changes on `export`ed TypeScript `type`s as major changes, apart from ones marked as `@internal` in the JSDoc. ## Why is tRPC on such a high version already? When tRPC started and had very few users, we often iterated on the API design while being strict with semver. - The first 9 versions of tRPC were released in the first 8 months of the project. - [Version 10](https://trpc.io/blog/announcing-trpc-10) which we released 14 months after v9 should be seen as the real "version 2" of tRPC where we did any fundamental changes to the API decisions. _(2 is 10 in binary, amirite?)_ We expect the API to be stable now and are planning to release codemods for any breaking changes in the future, just like we did with the v9->v10 upgrade. --- ## Anything else you want to know? Please write a feature request on [GitHub](https://github.com/trpc/trpc/issues), write in [GitHub Discussions](https://github.com/trpc/trpc/discussions), or [Discord](https://trpc.io/discord). You're also free to suggest improvement of this page or any other page using the "Edit this page" button at the bottom of the page. -------------------------------------------------- --- id: further-reading title: Further Reading sidebar_label: Further Reading slug: /further-reading --- ## Who is this for? - tRPC is for full-stack typescripters. It makes it dead easy to write "endpoints", which you can safely use in your app. - It's designed for monorepos, as you need to export/import the type definitions from/to your server. - If you already work in a team where languages are mixed or have third-party consumers over whom you have no control, you should create a language-agnostic [GraphQL](https://graphql.org/)-API. ## Relationship to GraphQL If you already have a custom GraphQL-server for your project, you may not want to use tRPC. GraphQL is amazing; it's great to be able to make a flexible API where each consumer can pick just the data they need. The thing is, GraphQL isn't that easy to get right - [ACL](https://en.wikipedia.org/wiki/Access-control_list) needs to be solved on a per-type basis, complexity analysis, and performance are all non-trivial things. We've taken a lot of inspiration from GraphQL. If you've previously built GraphQL servers, you'll be familiar with the concepts of input types and resolvers. tRPC is a lot simpler and couples your server & website/app more tightly together (for good and for bad). It allows you to move quickly, make changes without having to update a schema, and avoid thinking about the ever-traversable graph. -------------------------------------------------- --- id: rpc title: HTTP RPC Specification sidebar_label: HTTP RPC Specification slug: /rpc --- ## Methods \<-> Type mapping | HTTP Method | Mapping | Notes | | ----------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET` | `.query()` | Input JSON-stringified in query param.
_e.g._ `myQuery?input=${encodeURIComponent(JSON.stringify(input))}` | | `POST` | `.mutation()` | Input as POST body. | | `GET` | `.subscription()` | Subscriptions are supported via [Server-sent Events](/docs/client/links/httpSubscriptionLink) using `httpSubscriptionLink`, or via [WebSockets](/docs/server/websockets) using `wsLink`. | ## Accessing nested procedures Nested procedures are separated by dots, so a request to `byId` below would end up being a request to `/api/trpc/post.byId`. ```ts twoslash import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const router = t.router; const publicProcedure = t.procedure; // ---cut--- export const appRouter = router({ post: router({ byId: publicProcedure.input(String).query(async (opts) => { // [...] }), }), }); ``` ## Batching When batching, we combine all parallel procedure calls of the same HTTP method in one request using a data loader. - The called procedures' names are combined by a comma (`,`) in the `pathname` - Input parameters are sent as a query parameter called `input` which has the shape `Record`. - We also need to pass `batch=1` as a query parameter. - If the response has different statuses, we send back `207 Multi-Status` _(e.g., if one call errored and one succeeded)_ ### Batching Example Request #### Given a router like this exposed at `/api/trpc`: ```tsx twoslash title='server/router.ts' import { initTRPC } from '@trpc/server'; type Post = { id: string; title: string; body: string }; type Context = { post: { findUnique: (opts: { where: { id: string } }) => Promise }; findRelatedPostsById: (id: string) => Promise; }; const t = initTRPC.context().create(); // ---cut--- export const appRouter = t.router({ postById: t.procedure.input(String).query(async (opts) => { const post = await opts.ctx.post.findUnique({ where: { id: opts.input }, }); return post; }), relatedPosts: t.procedure.input(String).query(async (opts) => { const posts = await opts.ctx.findRelatedPostsById(opts.input); return posts; }), }); ``` #### ... And two queries defined like this in a React component: ```tsx twoslash title='MyComponent.tsx' // @jsx: react-jsx import React from 'react'; const trpc = null as any; // ---cut--- export function MyComponent() { const post1 = trpc.postById.useQuery('1'); const relatedPosts = trpc.relatedPosts.useQuery('1'); return (
      {JSON.stringify(
        {
          post1: post1.data ?? null,
          relatedPosts: relatedPosts.data ?? null,
        },
        null,
        4,
      )}
    
); } ``` #### The above would result in exactly 1 HTTP call with this data: | Location property | Value | | ----------------- | --------------------------------------------------------------- | | `pathname` | `/api/trpc/postById,relatedPosts` | | `search` | `?batch=1&input=%7B%220%22%3A%221%22%2C%221%22%3A%221%22%7D` \* | **\*) `input` in the above is the result of:** ```ts twoslash encodeURIComponent( JSON.stringify({ 0: '1', // <-- input for `postById` 1: '1', // <-- input for `relatedPosts` }), ); ``` ### Batching Example Response
Example output from server ```json [ // result for `postById` { "result": { "data": { "id": "1", "title": "Hello tRPC", "body": "..." // ... } } }, // result for `relatedPosts` { "result": { "data": [ /* ... */ ] } } ] ```
## HTTP Response Specification In order to have a specification that works regardless of the transport layer we try to conform to [JSON-RPC 2.0](https://www.jsonrpc.org/specification) where possible. ### Successful Response
Example JSON Response ```json { "result": { "data": { "id": "1", "title": "Hello tRPC", "body": "..." } } } ```
```ts twoslash type TOutput = any; // ---cut--- interface SuccessResponse { result: { data: TOutput; // output from procedure } } ``` ### Error Response
Example JSON Response ```json [ { "error": { "json": { "message": "Something went wrong", "code": -32600, // JSON-RPC 2.0 code "data": { // Extra, customizable, meta data "code": "INTERNAL_SERVER_ERROR", "httpStatus": 500, "stack": "...", "path": "post.add" } } } } ] ```

- When possible, we propagate HTTP status codes from the error thrown. - If the response has different statuses, we send back `207 Multi-Status` _(e.g., if one call errored and one succeeded)_ - For more on errors and how to customize them see [Error Formatting](../server/error-formatting.md). ## Error Codes \<-> HTTP Status ```ts twoslash const HTTP_STATUS_CODES = { PARSE_ERROR: 400, BAD_REQUEST: 400, UNAUTHORIZED: 401, PAYMENT_REQUIRED: 402, FORBIDDEN: 403, NOT_FOUND: 404, METHOD_NOT_SUPPORTED: 405, TIMEOUT: 408, CONFLICT: 409, PRECONDITION_FAILED: 412, PAYLOAD_TOO_LARGE: 413, UNSUPPORTED_MEDIA_TYPE: 415, UNPROCESSABLE_CONTENT: 422, PRECONDITION_REQUIRED: 428, TOO_MANY_REQUESTS: 429, CLIENT_CLOSED_REQUEST: 499, INTERNAL_SERVER_ERROR: 500, NOT_IMPLEMENTED: 501, BAD_GATEWAY: 502, SERVICE_UNAVAILABLE: 503, GATEWAY_TIMEOUT: 504, } as const; ``` ## Error Codes \<-> JSON-RPC 2.0 Error Codes
Available codes & JSON-RPC code ```ts twoslash /** * JSON-RPC 2.0 Error codes * * `-32000` to `-32099` are reserved for implementation-defined server-errors. * For tRPC we're copying the last digits of HTTP 4XX errors. */ export const TRPC_ERROR_CODES_BY_KEY = { /** * Invalid JSON was received by the server. * An error occurred on the server while parsing the JSON text. */ PARSE_ERROR: -32700, /** * The JSON sent is not a valid Request object. */ BAD_REQUEST: -32600, // 400 // Internal JSON-RPC error INTERNAL_SERVER_ERROR: -32603, // 500 NOT_IMPLEMENTED: -32603, // 501 BAD_GATEWAY: -32603, // 502 SERVICE_UNAVAILABLE: -32603, // 503 GATEWAY_TIMEOUT: -32603, // 504 // Implementation specific errors UNAUTHORIZED: -32001, // 401 PAYMENT_REQUIRED: -32002, // 402 FORBIDDEN: -32003, // 403 NOT_FOUND: -32004, // 404 METHOD_NOT_SUPPORTED: -32005, // 405 TIMEOUT: -32008, // 408 CONFLICT: -32009, // 409 PRECONDITION_FAILED: -32012, // 412 PAYLOAD_TOO_LARGE: -32013, // 413 UNSUPPORTED_MEDIA_TYPE: -32015, // 415 UNPROCESSABLE_CONTENT: -32022, // 422 PRECONDITION_REQUIRED: -32028, // 428 TOO_MANY_REQUESTS: -32029, // 429 CLIENT_CLOSED_REQUEST: -32099, // 499 } as const; ```
### Overriding the default HTTP method To override the HTTP method used for queries/mutations, you can use the `methodOverride` option: ```tsx twoslash title = 'server/httpHandler.ts' import { initTRPC } from '@trpc/server'; import { createHTTPHandler } from '@trpc/server/adapters/standalone'; const t = initTRPC.create(); const router = t.router({}); // ---cut--- // Your server must separately allow the client to override the HTTP method const handler = createHTTPHandler({ router: router, allowMethodOverride: true, }); ``` ```tsx twoslash title = 'client/trpc.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpLink } from '@trpc/client'; import type { AppRouter } from './server'; // The client can then specify which HTTP method to use for all queries/mutations const client = createTRPCClient({ links: [ httpLink({ url: `http://localhost:3000`, methodOverride: 'POST', // all queries and mutations will be sent to the tRPC Server as POST requests. }), ], }); ``` ## Dig deeper You can read more details by drilling into the TypeScript definitions in - [/packages/server/src/unstable-core-do-not-import/rpc/envelopes.ts](https://github.com/trpc/trpc/tree/main/packages/server/src/unstable-core-do-not-import/rpc/envelopes.ts) - [/packages/server/src/unstable-core-do-not-import/rpc/codes.ts](https://github.com/trpc/trpc/tree/main/packages/server/src/unstable-core-do-not-import/rpc/codes.ts) -------------------------------------------------- ```ts twoslash import { initTRPC } from '@trpc/server'; import z from 'zod'; // ---cut--- const t = initTRPC.create(); const router = t.router; const publicProcedure = t.procedure; const appRouter = router({ greeting: publicProcedure .input(z.object({ name: z.string() })) .query((opts) => { const { input } = opts; // ^? return `Hello ${input.name}` as const; }), }); export type AppRouter = typeof appRouter; ``` -------------------------------------------------- ```ts twoslash import { initTRPC } from '@trpc/server'; import { createHTTPServer } from '@trpc/server/adapters/standalone'; const t = initTRPC.create(); const appRouter = t.router({}); // ---cut--- const { listen } = createHTTPServer({ router: appRouter, }); // The API will now be listening on port 3000! listen(3000); ``` -------------------------------------------------- ```twoslash include server // @target: esnext // @filename: server.ts // ---cut--- import { initTRPC } from '@trpc/server'; import z from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ greeting: t.procedure .input(z.object({ name: z.string() })) .query((opts) => { const { input } = opts; return `Hello ${input.name}` as const; }), }); export type AppRouter = typeof appRouter; ``` ```ts twoslash // @target: esnext // @include: server // @filename: client.ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; // ---cut--- const trpc = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000', }), ], }); const res = await trpc.greeting.query({ name: 'John' }); // ^? ``` -------------------------------------------------- --- id: concepts title: Concepts sidebar_label: Concepts slug: /concepts --- import { ConceptsChart } from '@site/src/components/ConceptsChart'; ## What is RPC? What mindset should I adopt? ### It's just functions RPC is short for "Remote Procedure Call". It is a way of calling functions on one computer (the server) from another computer (the client). With traditional HTTP/REST APIs, you call a URL and get a response. With RPC, you call a function and get a response. ```ts // HTTP/REST const res = await fetch('/api/users/1'); const user = await res.json(); // RPC const user = await api.users.getById({ id: 1 }); ``` tRPC (TypeScript Remote Procedure Call) is one implementation of RPC, designed for TypeScript monorepos. It has its own flavor, but is RPC at its heart. ### Don't think about HTTP/REST implementation details If you inspect the network traffic of a tRPC app, you'll see that it's fairly standard HTTP requests and responses, but you don't need to think about the implementation details while writing your application code. You call functions, and tRPC takes care of everything else. You should ignore details like HTTP Verbs, since they carry meaning in REST APIs but, in RPC, form part of your function names instead, for instance: `getUser(id)` instead of `GET /users/:id`. ## Vocabulary Below are some terms that are used frequently in the tRPC ecosystem. We'll be using these throughout the documentation, so it's good to get familiar with them. Most of these concepts also have their own pages in the documentation. -------------------------------------------------- --- id: example-apps title: Example Apps sidebar_label: Example Apps slug: /example-apps description: Example apps built with tRPC --- ## Official ### Node.js | Example | Description | Links | | --------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Minimal** | Hello world server and client | [Source](https://github.com/trpc/trpc/tree/main/examples/minimal) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/minimal) | | **Standalone Server** | Custom HTTP server with WebSocket support | [Source](https://github.com/trpc/trpc/tree/main/examples/standalone-server) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/standalone-server) | | **Content Types** | Binary, text, and JSON content type handling | [Source](https://github.com/trpc/trpc/tree/main/examples/minimal-content-types) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/minimal-content-types) | | **Lazy Loading** | Lazy-loading routes with dynamic imports | [Source](https://github.com/trpc/trpc/tree/main/examples/lazy-load) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/lazy-load) | | **SOA** | Service-oriented architecture with multiple tRPC services | [Source](https://github.com/trpc/trpc/tree/main/examples/soa) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/soa) | | **OpenAPI Codegen** | Generate a typed REST client from a tRPC router via OpenAPI | [Source](https://github.com/trpc/trpc/tree/main/examples/openapi-codegen) | | **Kitchen Sink** | Comprehensive examples covering many features | [Source](https://github.com/trpc/examples-kitchen-sink) · [CodeSandbox](https://codesandbox.io/s/github/trpc/examples-kitchen-sink) | ### React | Example | Description | Links | | ------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Minimal React** | Node.js server + Vite React client monorepo | [Source](https://github.com/trpc/trpc/tree/main/examples/minimal-react) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/minimal-react) | | **Express + React** | Express server with React client using React Query | [Source](https://github.com/trpc/trpc/tree/main/examples/express-server) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/express-server) | ### Next.js | Example | Description | Links | | --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Prisma Starter** | Full-stack with Prisma, E2E testing, ESLint, and Tailwind CSS | [Source](https://github.com/trpc/examples-next-prisma-starter) · [Demo](https://nextjs.trpc.io) · [CodeSandbox](https://codesandbox.io/s/github/trpc/examples-next-prisma-starter) | | **Minimal Starter** | Bare-bones Next.js + tRPC setup | [Source](https://github.com/trpc/trpc/tree/main/examples/next-minimal-starter) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/next-minimal-starter) | | **TodoMVC** | TodoMVC with SSG and Prisma | [Source](https://github.com/trpc/examples-next-prisma-todomvc) · [Demo](https://todomvc.trpc.io) · [CodeSandbox](https://codesandbox.io/s/github/trpc/examples-next-prisma-todomvc) | | **WebSockets** | Prisma, WebSockets, NextAuth, and subscriptions | [Source](https://github.com/trpc/examples-next-prisma-websockets-starter) · [Demo](https://websockets.trpc.io) · [CodeSandbox](https://codesandbox.io/s/github/trpc/examples-next-prisma-websockets-starter) | | **SSE Chat** | Streaming chat with Server-Sent Events and Drizzle ORM | [Source](https://github.com/trpc/trpc/tree/main/examples/next-sse-chat) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/next-sse-chat) | | **Big Router** | Large router with code generation | [Source](https://github.com/trpc/trpc/tree/main/examples/next-big-router) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/next-big-router) | | **Edge Runtime** | tRPC on Next.js Edge Runtime | [Source](https://github.com/trpc/trpc/tree/main/examples/next-edge-runtime) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/next-edge-runtime) | | **FormData** | Form handling with React Hook Form and Zod | [Source](https://github.com/trpc/trpc/tree/main/examples/next-formdata) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/next-formdata) | | **WebSocket Encoder** | WebSockets with MessagePack binary encoding | [Source](https://github.com/trpc/trpc/tree/main/examples/next-websockets-encoder) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/next-websockets-encoder) | ### Serverless | Example | Description | Links | | ------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Lambda + API Gateway** | AWS Lambda with API Gateway | [Source](https://github.com/trpc/trpc/tree/main/examples/lambda-api-gateway) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/lambda-api-gateway) | | **Lambda Streaming** | AWS Lambda + API Gateway with streaming responses | [Source](https://github.com/trpc/trpc/tree/main/examples/lambda-api-gateway-streaming) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/lambda-api-gateway-streaming) | | **Lambda Function URLs** | AWS Lambda with Function URLs | [Source](https://github.com/trpc/trpc/tree/main/examples/lambda-url) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/lambda-url) | | **Cloudflare Workers** | tRPC on Cloudflare Workers | [Source](https://github.com/trpc/trpc/tree/main/examples/cloudflare-workers) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/cloudflare-workers) | | **Vercel Edge Runtime** | tRPC on Vercel Edge Runtime | [Source](https://github.com/trpc/trpc/tree/main/examples/vercel-edge-runtime) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/vercel-edge-runtime) | | **Deno Deploy** | tRPC client on Deno Deploy | [Source](https://github.com/trpc/trpc/tree/main/examples/deno-deploy) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/deno-deploy) | ### Other Ecosystems | Example | Description | Links | | ----------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Express** | Minimal Express + tRPC setup | [Source](https://github.com/trpc/trpc/tree/main/examples/express-minimal) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/express-minimal) | | **Fastify** | Fastify with WebSocket support | [Source](https://github.com/trpc/trpc/tree/main/examples/fastify-server) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/fastify-server) | | **Bun** | tRPC server and client on Bun | [Source](https://github.com/trpc/trpc/tree/main/examples/bun) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/bun) | | **Nuxt 3** | Nuxt 3 integration with trpc-nuxt | [Source](https://github.com/trpc/trpc/tree/main/examples/nuxt) · [CodeSandbox](https://codesandbox.io/s/github/trpc/trpc/tree/main/examples/nuxt) | ## Community :::note Community examples are maintained by their respective authors and may not be updated for the latest tRPC version. Check each project's README for version compatibility. ::: ### Frameworks | Example | Description | Links | | ---------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | **create-t3-turbo** | T3 Stack with Expo and Turborepo | [Source](https://github.com/t3-oss/create-t3-turbo) · [CodeSandbox](https://codesandbox.io/s/github/t3-oss/create-t3-turbo) | | **SvelteKit + Prisma** | SvelteKit integration with Prisma | [Source](https://github.com/icflorescu/trpc-sveltekit-example) · [CodeSandbox](https://codesandbox.io/s/github/icflorescu/trpc-sveltekit-example) | ### Integrations | Example | Description | Links | | ------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | **Separate BE/FE** | Separate backend and frontend repositories | [Backend (Express)](https://github.com/mkosir/trpc-api-boilerplate) · [Frontend (Vite)](https://github.com/mkosir/trpc-fe-boilerplate-vite) | -------------------------------------------------- --- id: introduction title: tRPC hide_title: true sidebar_label: Introduction slug: / author: Alex / KATT 🐱 author_url: https://twitter.com/alexdotjs author_image_url: https://avatars1.githubusercontent.com/u/459267?s=460&v=4 ---

End-to-end typesafe APIs made easy

weekly downloads {' '} GitHub License {' '} GitHub Stars

## Introduction

tRPC lets you build & consume fully typesafe APIs without schemas or code generation. It combines concepts from [REST](https://www.sitepoint.com/rest-api/) and [GraphQL](https://graphql.org/) - if you are unfamiliar with either, take a look at the key [Concepts](./concepts.mdx).

In full-stack TypeScript projects, keeping API contracts in sync between the client and server is a common pain point. tRPC does this by leveraging TypeScript's type inference directly, with no code generation step, and catches problems at build time. tRPC can run standalone or mounted as an endpoint on your existing REST API using our extensive ecosystem of adapters. ## Features - ✅  Well-tested and production ready. - 🧙‍♂️  Full static typesafety & autocompletion on the client, for inputs, outputs, and errors. - 🐎  Snappy DX - No code generation, run-time bloat, or build pipeline. - 🍃  Light - tRPC has zero runtime dependencies and a tiny client-side footprint. - 🐻  For new and old projects - Easy to start with or add to your existing brownfield project. - 🔋  Framework agnostic - The tRPC community has built [adapters](https://trpc.io/docs/awesome-trpc#-extensions--community-add-ons) for all of the most popular frameworks. - 🥃  Subscriptions support - Add typesafe real-time updates to your application. - ⚡️  Request batching - Requests made at the same time can be automatically combined into one. - 👀  Examples - Check out an [example](example-apps.mdx) to learn with or use as a starting point. ## Quick Look - [tRPC in 100 Seconds](https://www.youtube.com/watch?v=0DyAyLdVW0I) - [Matt Pocock: Learn tRPC in 5 minutes](https://www.youtube.com/watch?v=S6rcrkbsDI0) - [Chris Bautista: Making typesafe APIs easy with tRPC](https://www.youtube.com/watch?v=2LYM8gf184U) See more on the [Videos & Community Resources](./videos-and-community-resources.mdx) page. ## Try tRPC - [Minimal Example](https://stackblitz.com/github/trpc/trpc/tree/main/examples/minimal?file=server%2Findex.ts&file=client%2Findex.ts&view=editor) — Node.js http server + client. - [Minimal Next.js Example](https://stackblitz.com/github/trpc/trpc/tree/main/examples/next-minimal-starter?file=src%2Fpages%2Fapi%2Ftrpc%2F[trpc].ts&file=src%2Fpages%2Findex.tsx) — single endpoint + page. Or use an [example app](./example-apps.mdx) to get started locally. ## Adopt tRPC ### Creating a new project Since tRPC can live inside of many different frameworks, you will first need to decide where you want to use it. On the backend, there are [adapters](../server/adapters-intro.md) for a range of frameworks as well as vanilla Node.js. On the frontend, you can use our [TanStack React Query](../client/tanstack-react-query/setup.mdx) or [Next.js](../client/nextjs/overview.mdx) integrations, a [third-party integration](../community/awesome-trpc.mdx#frontend-frameworks) for a variety of other frameworks, or the [Vanilla Client](../client/vanilla/setup.mdx), which works anywhere JavaScript runs. After choosing your stack, you can either scaffold your app using a [template](./example-apps.mdx), or start from scratch using the documentation for your chosen backend and frontend integration. ### Adding tRPC to an existing project Adding tRPC to an existing project is not significantly different from starting a new project, so the same resources apply. The main challenge is that it can feel difficult to know how to integrate tRPC with your existing application. Here are some tips: - You don't need to port all of your existing backend logic to tRPC. A common migration strategy is to initially only use tRPC for new endpoints, and only later migrate existing endpoints to tRPC. - If you're not sure where to start, check the documentation for your backend [adapter](../server/adapters-intro.md) and frontend implementation, as well as the [example apps](./example-apps.mdx). - If you are looking for some inspiration of how tRPC might look as part of a larger codebase, there are some examples in [Open-source projects using tRPC](../community/awesome-trpc.mdx#-open-source-projects-using-trpc). ## Community Join us on [Discord](https://trpc.io/discord) to ask questions and share your experiences! -------------------------------------------------- --- id: quickstart title: Quickstart sidebar_label: Quickstart slug: /quickstart description: Learn how to quickly get started and setup tRPC --- import CodeBlock from '@theme/CodeBlock'; import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs'; ```twoslash include trpc import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const router = t.router; export const publicProcedure = t.procedure; ``` ```twoslash include appRouter import { z } from "zod"; import { publicProcedure, router } from "./trpc"; type User = { id: string; name: string }; export const appRouter = router({ userList: publicProcedure .query(async () => { const users: User[] = [{ id: '1', name: 'Katt' }]; return users; }), userById: publicProcedure .input(z.string()) .query(async (opts) => { const { input } = opts; const user: User = { id: input, name: 'Katt' }; return user; }), userCreate: publicProcedure .input(z.object({ name: z.string() })) .mutation(async (opts) => { const { input } = opts; const user: User = { id: '1', ...input }; return user; }), }); export type AppRouter = typeof appRouter; ``` ```twoslash include server import { createHTTPServer } from "@trpc/server/adapters/standalone"; import { appRouter } from "./appRouter"; const server = createHTTPServer({ router: appRouter, }); server.listen(3000); ``` ## Installation tRPC is split between several packages, so you can install only what you need. Make sure to install the packages you want in the proper sections of your codebase. For this quickstart guide we'll keep it simple and use the vanilla client only. For framework guides, check out [usage with React](/docs/client/tanstack-react-query/setup.mdx) and [usage with Next.js](/docs/client/nextjs/overview.mdx). :::info Requirements - tRPC requires TypeScript >=5.7.2 - We strongly recommend using `"strict": true` in your `tsconfig.json` as we don't officially support non-strict mode. ::: Start off by installing the `@trpc/server` and `@trpc/client` packages: import { InstallSnippet } from '@site/src/components/InstallSnippet'; :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: ## Your first tRPC API Let's walk through the steps of building a typesafe API with tRPC. To start, this API will contain three endpoints with these TypeScript signatures: ```ts type User = { id: string; name: string; }; userList: () => User[]; userById: (id: string) => User; userCreate: (data: { name: string }) => User; ``` Here's the file structure we'll be building. We recommend separating tRPC initialization, router definition, and server setup into distinct files to prevent cyclic dependencies: ``` . ├── server/ │ ├── trpc.ts # tRPC instantiation & setup │ ├── appRouter.ts # Your API logic and type export │ └── index.ts # HTTP server └── client/ └── index.ts # tRPC client ``` ### 1. Create a router instance First, let's initialize the tRPC backend. It's good convention to do this in a separate file and export reusable helper functions instead of the entire tRPC object. ```ts twoslash title='server/trpc.ts' import { initTRPC } from '@trpc/server'; /** * Initialization of tRPC backend * Should be done only once per backend! */ const t = initTRPC.create(); /** * Export reusable router and procedure helpers * that can be used throughout the router */ export const router = t.router; export const publicProcedure = t.procedure; ``` Next, we'll initialize our main router instance, commonly referred to as `appRouter`, to which we'll later add procedures. Lastly, we need to export the type of the router which we'll later use on the client side. ```ts twoslash title='server/appRouter.ts' // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts // ---cut--- import { router } from './trpc'; export const appRouter = router({ // ... }); export type AppRouter = typeof appRouter; ``` ### 2. Add a query procedure Use `publicProcedure.query()` to add a query procedure to the router. The following creates a query procedure called `userList` that returns a list of users: ```ts twoslash title='server/appRouter.ts' // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts type User = { id: string; name: string }; // ---cut--- import { publicProcedure, router } from './trpc'; export const appRouter = router({ userList: publicProcedure .query(async () => { const users: User[] = [{ id: '1', name: 'Katt' }]; return users; }), }); export type AppRouter = typeof appRouter; ``` ### 3. Using input parser to validate procedure inputs To implement the `userById` procedure, we need to accept input from the client. tRPC lets you define [input parsers](../server/validators.md) to validate and parse the input. You can define your own input parser or use a validation library of your choice, like [zod](https://zod.dev), [yup](https://github.com/jquense/yup), or [superstruct](https://docs.superstructjs.org/). You define your input parser on `publicProcedure.input()`, which can then be accessed on the resolver function as shown below: The input parser should be a function that validates and casts the input of this procedure. It should return a strongly typed value when the input is valid or throw an error if the input is invalid.
:::info Throughout the remainder of this documentation, we will use `zod` as our validation library. ::: ```ts twoslash title='server/appRouter.ts' // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts type User = { id: string; name: string }; // ---cut--- import { publicProcedure, router } from './trpc'; export const appRouter = router({ // ... userById: publicProcedure // The input is unknown at this time. A client could have sent // us anything so we won't assume a certain data type. .input((val: unknown) => { // If the value is of type string, return it. // It will now be inferred as a string. if (typeof val === 'string') return val; // Uh oh, looks like that input wasn't a string. // We will throw an error instead of running the procedure. throw new Error(`Invalid input: ${typeof val}`); }) .query(async (opts) => { const { input } = opts; // ^? const user: User = { id: input, name: 'Katt' }; return user; }), }); export type AppRouter = typeof appRouter; ```
The input parser can be any ZodType, e.g. z.string() or z.object({}).
```ts twoslash title='server/appRouter.ts' // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts type User = { id: string; name: string }; // ---cut--- import { publicProcedure, router } from './trpc'; import { z } from 'zod'; export const appRouter = router({ // ... userById: publicProcedure .input(z.string()) .query(async (opts) => { const { input } = opts; // ^? const user: User = { id: input, name: 'Katt' }; return user; }), }); export type AppRouter = typeof appRouter; ```
The input parser can be any YupSchema, e.g. yup.string() or yup.object({}).
:::info Throughout the remainder of this documentation, we will use `zod` as our validation library. ::: ```ts twoslash title='server/appRouter.ts' // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts type User = { id: string; name: string }; // ---cut--- import { publicProcedure, router } from './trpc'; import * as yup from 'yup'; export const appRouter = router({ // ... userById: publicProcedure .input(yup.string().required()) .query(async (opts) => { const { input } = opts; // ^? const user: User = { id: input, name: 'Katt' }; return user; }), }); export type AppRouter = typeof appRouter; ```
The input parser can be any Valibot schema, e.g. v.string() or v.object({}).
:::info Throughout the remainder of this documentation, we will use `zod` as our validation library. ::: ```ts twoslash title='server/appRouter.ts' // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts type User = { id: string; name: string }; // ---cut--- import { publicProcedure, router } from './trpc'; import * as v from 'valibot'; export const appRouter = router({ // ... userById: publicProcedure .input(v.string()) .query(async (opts) => { const { input } = opts; // ^? const user: User = { id: input, name: 'Katt' }; return user; }), }); export type AppRouter = typeof appRouter; ```
### 4. Adding a mutation procedure Similar to GraphQL, tRPC makes a distinction between Query and Mutation procedures. The distinction between a Query and a Mutation is primarily semantic. Queries use HTTP GET and are intended for read operations, while Mutations use HTTP POST and are intended for operations that cause side effects. Let's add a `userCreate` mutation by adding it as a new property on our router object: ```ts twoslash title='server/appRouter.ts' // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts import { z } from 'zod'; type User = { id: string; name: string }; // ---cut--- import { publicProcedure, router } from './trpc'; export const appRouter = router({ // ... userCreate: publicProcedure .input(z.object({ name: z.string() })) .mutation(async (opts) => { const { input } = opts; // ^? // Create the user in your DB const user: User = { id: '1', ...input }; return user; }), }); export type AppRouter = typeof appRouter; ``` ## Serving the API Now that we have defined our router, we can serve it. tRPC has first-class [adapters](../server/adapters-intro.md) for many popular web servers. To keep it simple, we'll use the [`standalone`](../server/adapters/standalone.md) Node.js adapter here. ```ts twoslash title='server/index.ts' // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts // @include: appRouter // @filename: server.ts // ---cut--- import { createHTTPServer } from '@trpc/server/adapters/standalone'; import { appRouter } from './appRouter'; const server = createHTTPServer({ router: appRouter, }); server.listen(3000); ```
See the full backend code ```ts twoslash title="server/trpc.ts" // @include: trpc ```
```ts twoslash title='server/appRouter.ts' // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts // ---cut--- // @include: appRouter ```
```ts twoslash title='server/index.ts' // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts // @include: appRouter // @filename: server.ts // ---cut--- // @include: server ```
## Using your new backend on the client Let's now move to the client-side code and embrace the power of end-to-end typesafety. When we import the `AppRouter` type for the client to use, we have achieved full typesafety for our system without leaking any implementation details to the client. ### 1. Setup the tRPC Client ```ts twoslash title="client/index.ts" // @target: esnext // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts // @include: appRouter // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './appRouter'; // 👆 **type-only** imports are stripped at build time // Pass AppRouter as a type parameter. 👇 This lets `trpc` know // what procedures are available on the server and their input/output types. const trpc = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000', }), ], }); ``` Links in tRPC are similar to links in GraphQL, they let us control the data flow to the server. In the example above, we use the [httpBatchLink](../client/links/httpBatchLink.md), which automatically batches up multiple calls into a single HTTP request. For more in-depth usage of links, see the [links documentation](../client/links/overview.md). ### 2. Type Inference & Autocomplete You now have access to your API procedures on the `trpc` object. Try it out! ```ts twoslash title="client/index.ts" // @target: esnext // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts // @include: appRouter // @filename: client.ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './appRouter'; const trpc = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000', }), ], }); // ---cut--- // Inferred types const user = await trpc.userById.query('1'); // ^? const createdUser = await trpc.userCreate.mutate({ name: 'Katt' }); // ^? ``` You can also use your autocomplete to explore the API on your client ```ts twoslash title="client/index.ts" // @target: esnext // @filename: trpc.ts // @include: trpc // @filename: appRouter.ts // @include: appRouter // @filename: client.ts import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './appRouter'; const trpc = createTRPCClient({ links: [ httpBatchLink({ url: 'http://localhost:3000', }), ], }); // ---cut--- // @errors: 2339 trpc.u; // ^| ``` ## Next steps | What's next? | Description | |---|---| | [Example Apps](example-apps.mdx) | Explore tRPC in your chosen framework | | [TanStack React Query](../client/tanstack-react-query/setup.mdx) | Recommended React integration via `@trpc/tanstack-react-query` | | [Next.js](../client/nextjs/overview.mdx) | Usage with Next.js | | [Server Adapters](../server/adapters-intro.md) | Express, Fastify, and more | | [Transformers](../server/data-transformers.md#using-superjson) | Use superjson to retain complex types like `Date` | -------------------------------------------------- --- id: skills title: Agent Skills sidebar_label: Agent Skills slug: /skills description: Set up and use TanStack Intent to provide AI agent skills for tRPC --- # Agent Skills tRPC ships with [TanStack Intent](https://tanstack.com/intent/latest/docs/getting-started/quick-start-consumers) skills to help AI coding agents work with tRPC. When your agent works on a task that matches a skill mapping, the corresponding skill file is automatically loaded into context. ## Setup ### 1. Run install The `install` command guides your agent through setup: ```bash npx @tanstack/intent@latest install ``` This prints a prompt that instructs your AI agent to configure itself to access the skills shipped in tRPC and your other installed packages. ### 2. Use skills in your workflow When your agent works on a task that matches a mapping, it automatically loads the corresponding `SKILL.md` into context to guide implementation. ### 3. Keep skills up-to-date Skills version with library releases. When you update a library (e.g. `npm update @trpc/server`), the new version brings updated skills automatically. The skills are shipped with the library, so you always get the version that matches your installed code. To see what skills are available: ```bash npx @tanstack/intent@latest list ``` To check if any skills reference outdated source documentation: ```bash npx @tanstack/intent@latest stale ``` ### 4. Submit feedback (optional) After using a skill, you can submit feedback to help maintainers improve it: ```bash npx @tanstack/intent@latest meta feedback-collection ``` This prints a prompt that guides your agent to collect structured feedback about gaps, errors, and improvements. ## Learn more For full documentation on TanStack Intent, see the [Quick Start for Consumers](https://tanstack.com/intent/latest/docs/getting-started/quick-start-consumers) guide. -------------------------------------------------- --- id: videos-and-community-resources title: Videos and Community Resources sidebar_label: Videos & Community Resources slug: /videos-and-community-resources --- import { YouTubeEmbed } from '@site/src/components/YouTubeEmbed'; ### tRPC in 100 Seconds ### Matt Pocock: Learn tRPC in 5 minutes ### Chris Bautista: Making typesafe APIs easy with tRPC 15 minute video explaining the basics of tRPC and showing an example app. ### How tRPC really works 20 minute video showing how data flows in tRPC and explaining some core concepts. This video uses Next.js, but the concepts apply to any implementation. ### T3: tRPC, Prisma and NextAuth Done Right 45 minute tutorial by Jack Herrington that builds an app with Next.js, tRPC, Prisma, and NextAuth.js. ### DevTools.FM Episode 21 60 minute podcast episode with Alex, the creator of tRPC. -------------------------------------------------- --- id: migrate-from-v10-to-v11 title: Migrate from v10 to v11 sidebar_label: Migrate from v10 to v11 slug: /migrate-from-v10-to-v11 --- import { InstallSnippet } from '@site/src/components/InstallSnippet'; ## Migrating from v10 to v11 :::tip For most users, the migration should be quick & straight-forward. If the below three steps aren't enough, look through the below document for _"rarely breaking"_. ::: ### 1. Install new versions ### 2. If you're using `transformer`s, update your links See [transformers are moved to links](#transformers-moved) for more information. ### 3. If you're using `@trpc/react-query` update your `@tanstack/react-query`-version See [react-query-v5](#react-query-v5) for more information. ## Full reverse-chronological changelog ### New TanStack React Query integration! (non-breaking) We are excited to announce the new TanStack React Query integration for tRPC is now available in tRPC v11! {/* Uses a ``-link to prevent Docusaurus' build from failing */} Read the blog post for more information. ### Stopping subscriptions from the server (rarely breaking) We now support stopping subscriptions from the server, this means that you can now do things like this: ```ts twoslash // @types: node import EventEmitter, { on } from 'events'; import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const router = t.router; const publicProcedure = t.procedure; const ee = new EventEmitter(); // ---cut--- const myRouter = router({ sub: publicProcedure.subscription(async function* (opts) { for await (const data of on(ee, 'data', { signal: opts.signal, })) { const num = data[0] as number | undefined; if (num === undefined) { // This will now stop the subscription on the client and trigger the `onComplete` callback return; } yield num; } }), }); ``` See the [subscriptions docs](../server/subscriptions.md#stopping-from-server) for more information. ### Added support for lazy-loading routers (non-breaking) See the [lazy-loading routers docs](../server/merging-routers.md#lazy-load) for more information. > As part of this, we've changed the argument of the internal method `callProcedure()` to receive a `{ router: AnyRouter }`-param instead of a `{ _def: AnyRouter['_def'] }`-param. ### Custom `basePath` to handle requests in the standalone adapter (non-breaking) The standalone adapter now supports a `basePath` option, which will slice the basePath from the beginning of the request path. See the [standalone adapter docs](../server/adapters/standalone.md#custom-basePath) for more information. ### Added support for HTTP/2 servers (non-breaking) We now support HTTP/2 servers, this means that you can now use the `createHTTP2Handler` to create HTTP/2 servers and `createHTTPServer` to create HTTP/1 servers. See the [standalone adapter docs](../server/adapters/standalone.md#http2) for more information. ### Move `TRPCProcedureOptions` to `@trpc/client` (non-breaking for most) If you previously used `ProcedureOptions` from `@trpc/server`, you now need to use `TRPCProcedureOptions` from `@trpc/client` instead. ### Allow promises to be embedded in nested data (non-breaking) We now allow promises to be embedded in nested data when using the [`httpBatchStreamLink`](../client/links/httpBatchStreamLink.md), this means that you can now do things like this: ```ts twoslash import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const publicProcedure = t.procedure; // ---cut--- const appRouter = t.router({ embedPromise: publicProcedure.query(() => { async function slowThing() { await new Promise((resolve) => setTimeout(resolve, 1000)); return 'slow'; } return { instant: 'instant', slow: slowThing(), }; }), }); ``` ### Moved `reconnectAfterInactivityMs` to `sse.client` (non-breaking) Updated [HTTP Subscription Link improvements](#http-subscription-link-improvements)-section and related docs. ### TypeScript version >=5.7.2 is now required (non-breaking) tRPC now requires TypeScript version 5.7.2 or higher. This change was made in response to [a bug report](https://github.com/trpc/trpc/issues/6243) where we decided to take a forward-looking approach. If you try to install tRPC with an unsupported TypeScript version, you'll receive a peer dependency error during installation. If you notice your editor showing `any` types, it's likely because your editor isn't using the correct TypeScript version. To fix this, you'll need to configure your editor to use the TypeScript version installed in your project's `package.json`. For VSCode users, add these settings to your `.vscode/settings.json`: ```json title=".vscode/settings.json" { "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true } ``` ### Moves `experimental.sseSubscriptions` -> `sse` (non-breaking) The `experimental.sseSubscriptions` option has been moved to just `sse` in the `initTRPC.create()`-function. ### HTTP Subscription Link improvements (non-breaking) {#http-subscription-link-improvements} Added support for detecting and recovering from stale connections: On the server, you can configure a ping interval to keep the connection alive: ```ts twoslash import { initTRPC } from '@trpc/server'; export const t = initTRPC.create({ sse: { ping: { enabled: true, intervalMs: 15_000, }, client: { // Reconnect if no messages or pings are received for 20 seconds reconnectAfterInactivityMs: 20_000, }, }, }); ``` We will likely add a default ping interval and timeout configuration in the future, but this is not yet decided. Feedback is welcome in the [🎏-rfc-streaming](https://trpc.io/discord) channel on Discord. See the [`httpSubscriptionLink` docs](../client/links/httpSubscriptionLink.md#timeout) for more details on these features. ### Introduction of `retryLink` (non-breaking) See [retryLink](../client/links/retryLink.md) - allows you to retry failed operations ### `useSubscription` improvements (non-breaking) - When subscribing to procedures using the [useSubscription](../client/react/useSubscription.md) hook it will now return information about the status of the subscription and the connection. - Ability to have a ponyfill when using [`httpSubscriptionLink`](../client/links/httpSubscriptionLink.md) ### Subscription procedure output type changed to `AsyncGenerator` (non-breaking) If you've used subscriptions with async generators with the v11, this might be breaking with how you infer your types.
Details We changed the inferred output from: ```ts SubscriptionProcedure<{ input: __INPUT__; output: __OUTPUT__; }>; ``` to ```ts SubscriptionProcedure<{ input: __INPUT__; output: AsyncGenerator<__OUTPUT__, void, unknown>; }>; ``` If you need to infer the value you can use a helper like the below: ```ts twoslash // @target: esnext type inferAsyncIterableYield = TOutput extends AsyncGenerator ? $Yield : never; ```
This change has been made to ensure the library remains compatible with future updates and allows for the use of the `return` type in subscriptions' `AsyncGenerator`s. See [subscriptions docs](../server/subscriptions.md#output-validation) for more information. ### Added support for output validators in subscriptions (non-breaking) See [subscriptions docs](../server/subscriptions.md#output-validation) for more information. ### Deprecation of subscriptions returning `Observable`s (non-breaking) We now support returning async generator function for subscriptions and we previously added a [`httpSubscriptionLink`](../client/links/httpSubscriptionLink.md). To see how to use async generator functions for subscriptions see the [subscriptions docs](../server/subscriptions.md). ### Removal of `AbortControllerEsque`-ponyfill (rarely breaking) We have removed the `AbortControllerEsque`-ponyfill from tRPC, if you need to support older browsers you can use a polyfill like `abortcontroller-polyfill`. ### Support for Server-sent events (SSE) (non-breaking) We now support SSE for subscriptions, this means that you don't need to spin up a WebSocket server to get real-time updates in your application & that the client can automatically reconnect and resume if the connection is lost. 👉 [See more in the `httpSubscriptionLink` docs](../client/links/httpSubscriptionLink.md). ### Support for streaming responses over HTTP (non-breaking) We now support streaming mutations and queries using the [`httpBatchStreamLink`](../client/links/httpBatchStreamLink.md#generators). This means that query and mutation resolvers can either be [`AsyncGenerator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator)s with `yield` or return promises that can be deferred for later and you can use stream responses over HTTP, without using WebSockets. We want your feedback on this feature, so please try it out and let us know what you think in the [`🎏-rfc-streaming`-channel on our Discord](https://trpc.io/discord)! 👉 [See more in the `httpBatchStreamLink` docs](../client/links/httpBatchStreamLink.md#generators) ### `resolveHTTPRequest` has been replaced by `resolveRequest` that uses Fetch APIs (rarely breaking) The function `resolveHTTPRequest` has been replaced by `resolveRequest` which uses the Fetch API - [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)/[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). This is a breaking change for HTTP-adapters, but should not affect you as a user. If you're building an adapter, check out how our adapters work [in the code](https://github.com/trpc/trpc/tree/main/packages/server/src/adapters) and don't be a stranger to ask for help in our [Discord](https://trpc.io/discord). ### `TRPCRequestInfo` has been updated (rarely breaking) Inputs are now materialised lazily when required by the procedure, which means the input and procedure type is no longer available when tRPC calls `createContext`. You can still access the input by calling `info.calls[index].getRawInput()`. ### All the experimental form-data support has been replaced (rarely breaking) > This only affects you if you used the experimental formdata features - experimental_formDataLink - use httpLink - experimental_parseMultipartFormData - not needed anymore - experimental_isMultipartFormDataRequest - not needed anymore - experimental_composeUploadHandlers - not needed anymore - experimental_createMemoryUploadHandler - not needed anymore - experimental_NodeOnDiskFile and experimental_createFileUploadHandler - not supported in this first release, open an issue if you need to hold data on disk - experimental_contentTypeHandlers - not needed anymore, but could come back if needed by the community for novel data types You can see the new approach in `examples/next-formdata` ### Moved `Procedure._def._output_in` / `Procedure._def._input_in` to `Procedure._def.$types` (non-breaking) This is a breaking change for tRPC internals, but should not affect you as a user. You don't have to do anything, unless you're using `Procedure._def._output_in` or `Procedure._def._input_in` directly in your code. ### Explicit Content-Type checks (non-breaking) We now have explicit checks for the `Content-Type`-header when doing POST-requests. This means that if you send a request with a `Content-Type` that doesn't match the expected one, you will get a `415 Unsupported Media Type`-error. Our tRPC clients already sends content-type headers, so is only a potential breaking change if you call tRPC manually. ### Added support for method overriding (rarely breaking) Allows you to override the HTTP method for procedures to always be sent with `POST` in order to get around some limitations with e.g. max URL lengths. Closes [#3910](https://github.com/trpc/trpc/issues/3910) ### Added support for bi-directional infinite queries (non-breaking) See [`useInfiniteQuery()`](../client/react/useInfiniteQuery.md) ### Added `inferProcedureBuilderResolverOptions`-helper (non-breaking) Adds a helper to infer the options for a procedure builder resolver. This is useful if you want to create reusable functions for different procedures. See test [here](https://github.com/trpc/trpc/blob/743fa6aed8ac889d9c60f321c4b4ad060b56e791/packages/server/src/unstable-core-do-not-import/procedureBuilder.test.ts#L36-L231) for a reference on how to use it ### Transformers are moved to links (breaking) - {#transformers-moved} > TypeScript will guide you through this migration > > Only applies if you use data transformers. You now set up data transformers in the `links`-array instead of when you initialize the tRPC-client; Wherever you have a HTTP Link you have to add `transformer: superjson` if you use transformers: ```ts twoslash import { httpBatchLink } from '@trpc/client'; import superjson from 'superjson'; // ---cut--- httpBatchLink({ url: '/api/trpc', transformer: superjson, // <-- add this }); ``` ```ts import { createTRPCNext } from '@trpc/next'; import superjson from 'superjson'; import { AppRouter } from './appRouter' createTRPCNext({ // [..] transformer: superjson, // <-- add this }); ``` ### `@trpc/next` ssr mode now requires a prepass helper with `ssr: true` (rarely breaking) This is to fix https://github.com/trpc/trpc/issues/5378 where `react-dom` was imported regardless if you were using this functionality or not. See [SSR docs](../client/nextjs/pages-router/ssr.md) ### Added support for short-hand router definitions (non-breaking) See [Routers](../server/routers.md#inline-sub-router) ```ts twoslash import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); const router = t.router; const publicProcedure = t.procedure; // ---cut--- const appRouter = router({ // Shorthand plain object for creating a sub-router nested1: { proc: publicProcedure.query(() => '...'), }, // Equivalent of: nested2: router({ proc: publicProcedure.query(() => '...'), }), }); ``` ### Deleted `inferHandlerInput` and `ProcedureArgs` (non-breaking for most) > If these types mean nothing for you or your codebase, just ignore this Use `inferProcedureInput` instead & `TRPCProcedureOptions` instead ### Added `useSuspenseQueries()` See [useSuspenseQueries](../client/react/suspense.md#usesuspensequeries) https://github.com/trpc/trpc/pull/5226 ### Refactor internal generics (rarely breaking) We have refactored our internal generics and made them more readable. ### React is now >=18.2.0 (rarely breaking) Check their migration guide: https://react.dev/blog/2022/03/08/react-18-upgrade-guide ### NodeJS 18+ and Modern Browsers are now required (rarely breaking) We have added usage of FormData, File, Blob, and ReadableStream. NodeJS 18 is now required, though these have been supported by browsers for many years now. ### `wsLink` improvements (minor) - Ability to pass a `Promise` in the `url`-callback if servers switch location during deploys - Added new `lazy` option that makes the websocket automatically disconnect when there are no pending requests ### `rawInput` in middleware is now a `getRawInput` (rarely breaking) While we're not doing anything differently internally (just yet) this is to help support a much requested feature in tRPC: content types other than JSON. ### Simplified types and `.d.ts` outputs Procedures in your router now only emit their input & output - where before they would also contain the full context object for every procedure, leading to unnecessary complexity in e.g. `.d.ts`. ### React Query peerDep is now v5 (breaking) - {#react-query-v5} > The main thing you'll need to do is to replace a bunch of `isLoading` to `isPending` Check their migration guide: https://tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5 ### Exports names `AbcProxyXyz` has been renamed to `AbcXyz` (non-breaking) The proxy names were due to v9 using the `AbcXyz` names, these have been removed and the proxy ones have been renamed to the non-proxy names, e.g: - `createTRPCClient` was deprecated from v9, and is now completely removed. The `createTRPCProxyClient` has been renamed to `createTRPCClient` instead. `createTRPCProxyClient` is now marked as deprecated. ### SSG Helpers (rarely breaking) - `createSSGHelpers` were for v9 which has now been removed. the v10 equivalent `createProxySSGHelpers` have been renamed to `createSSGHelpers` now instead. - `createProxySSGHelpers` is now deprecated but aliased to `createSSGHelpers` for backwards compatibility. - Removed exported type `CreateSSGHelpersOptions` ### `interop`-mode has been removed (rarely breaking) - {#interop-mode-removed} We have removed the `interop`-mode from tRPC. This was a mode that allowed you to have an easy transition period from v9 to v10. This mode was never meant to be supported long-term and we have now removed it. -------------------------------------------------- Import your `AppRouter` type into the client application. This type holds the shape of your entire API. ```twoslash include router // @filename: server/router.ts // ---cut--- import { initTRPC } from '@trpc/server'; import { z } from "zod"; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ byId: t.procedure .input(z.object({ id: z.string() })) .query(async ({input}) => { return { id: input.id, title: 'Hello' }; }), }) }); export type AppRouter = typeof appRouter; ``` ```ts twoslash title="utils/trpc.ts" // @include: router // @filename: utils/trpc.ts // ---cut--- import type { AppRouter } from '../server/router'; ``` :::tip By using `import type` you ensure that the reference will be stripped at compile-time, meaning you don't inadvertently import server-side code into your client. For more information, [see the Typescript docs](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export). ::: -------------------------------------------------- --- id: aws-lambda title: AWS Lambda + API Gateway Adapter sidebar_label: AWS Lambda + API Gateway slug: /server/adapters/aws-lambda --- ## AWS Lambda adapter The AWS Lambda adapter is supported for API Gateway [REST API(v1)](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html) and [HTTP API(v2)](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html), and [Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html) use cases. > `httpBatchLink` requires the router to work on a single API Gateway Resource (as shown in the [example](https://github.com/trpc/trpc/tree/main/examples/lambda-api-gateway)). > If you'd like to have a Resource per procedure, you can use the `httpLink` instead ([more info](https://github.com/trpc/trpc/issues/5738#issuecomment-2130001522)). ## Example apps
Description Links
API Gateway with NodeJS client.
API Gateway REST API with response streaming.
## How to add tRPC ### 1. Install deps ```bash yarn add @trpc/server ``` :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: ### 2. Create a tRPC router Implement your tRPC router. A sample router is given below: ```ts twoslash title='server.ts' import { initTRPC } from '@trpc/server'; import { z } from 'zod'; export const t = initTRPC.create(); const appRouter = t.router({ getUser: t.procedure.input(z.string()).query((opts) => { opts.input; // string return { id: opts.input, name: 'Bilbo' }; }), }); // export type definition of API export type AppRouter = typeof appRouter; ``` ### 3. Use the Amazon API Gateway adapter tRPC includes an adapter for API Gateway out of the box. This adapter lets you run your routes through the API Gateway handler. ```ts twoslash title='server.ts' // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: server.ts // ---cut--- import type { APIGatewayProxyEventV2 } from 'aws-lambda'; import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda'; import { awsLambdaRequestHandler } from '@trpc/server/adapters/aws-lambda'; import { appRouter } from './router'; // created for each request const createContext = ({ event, context, }: CreateAWSLambdaContextOptions) => ({}); // no context type Context = Awaited>; export const handler = awsLambdaRequestHandler({ router: appRouter, createContext, }) ``` Build & deploy your code, now use your API Gateway URL to call your function. | Endpoint | HTTP URI | | --------- | ------------------------------------------------------------------------------------------------------------ | | `getUser` | `GET https:///getUser?input=INPUT`

where `INPUT` is a URI-encoded JSON string. | #### A word about payload format version API Gateway has two different event data formats when it invokes a Lambda. For REST APIs they should be version "1.0"(`APIGatewayProxyEvent`), but you can choose which for HTTP APIs by stating either version "1.0" or "2.0". - Version 1.0: `APIGatewayProxyEvent` - Version 2.0: `APIGatewayProxyEventV2` To infer what version you might have, supply the context as following: ```ts twoslash import type { APIGatewayProxyEvent } from 'aws-lambda'; import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda'; // ---cut--- function createContext({ event, context, }: CreateAWSLambdaContextOptions) { // ... } // CreateAWSLambdaContextOptions or CreateAWSLambdaContextOptions ``` [Read more here about payload format version](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) ## AWS Lambda Response Streaming Adapter AWS Lambda supports streaming responses to clients with both Lambda Function URLs and API Gateway REST APIs. > Response streaming is supported for Lambda Function URLs and API Gateway REST APIs. For API Gateway REST APIs, you need to configure the integration with `responseTransferMode: STREAM`. [Read more about Lambda response streaming](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/) and [API Gateway response streaming](https://aws.amazon.com/blogs/compute/building-responsive-apis-with-amazon-api-gateway-response-streaming/). ### Response Streaming The signature of a streaming handler is different from the default handler. The streaming handler additionally receives a writable stream parameter, `responseStream`, besides the default node handler parameters, `event` and `context`. To indicate that Lambda should stream your responses, you must wrap your function handler with the `awslambda.streamifyResponse()` decorator. > Note that the `awslambda` namespace is automatically provided by the Lambda execution environment. You can import the types from `@types/aws-lambda` to augment the global namespace with the `awslambda` namespace. ```ts twoslash title='server.ts' /// // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({ iterable: t.procedure.query(async function* () { for (let i = 0; i < 10; i++) { await new Promise((resolve) => setTimeout(resolve, 500)); yield i; } }), }); // @filename: server.ts // ---cut--- /// import type { APIGatewayProxyEventV2 } from 'aws-lambda'; import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda'; import { awsLambdaStreamingRequestHandler } from '@trpc/server/adapters/aws-lambda'; import { appRouter } from './router'; // created for each request const createContext = ({ event, context, }: CreateAWSLambdaContextOptions) => ({ // your context }); type Context = Awaited>; export const handler = awslambda.streamifyResponse( awsLambdaStreamingRequestHandler({ router: appRouter, createContext, }), ); ``` -------------------------------------------------- --- id: express title: Express Adapter sidebar_label: Express slug: /server/adapters/express --- ## Example app
Description Links
Express server & procedure calls with Node.js.
## How to add tRPC to existing Express project ### 1. Install deps ```bash yarn add @trpc/server zod ``` > [Zod](https://github.com/colinhacks/zod) isn't a required dependency, but it's used in the sample router below. :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: ### 2. Create a tRPC router Implement your tRPC router. A sample router is given below: ```ts twoslash title='server.ts' import { initTRPC } from '@trpc/server'; import { z } from 'zod'; export const t = initTRPC.create(); export const appRouter = t.router({ getUser: t.procedure.input(z.string()).query((opts) => { opts.input; // string return { id: opts.input, name: 'Bilbo' }; }), createUser: t.procedure .input(z.object({ name: z.string().min(5) })) .mutation(async (opts) => { // use your ORM of choice return { id: '1', ...opts.input }; }), }); // export type definition of API export type AppRouter = typeof appRouter; ``` If your router file starts getting too big, split your router into several subrouters each implemented in its own file. Then [merge them](/docs/server/merging-routers) into a single root `appRouter`. ### 3. Use the Express adapter tRPC includes an adapter for Express out of the box. This adapter lets you convert your tRPC router into an Express middleware. ```ts twoslash title='server.ts' import { initTRPC } from '@trpc/server'; import * as trpcExpress from '@trpc/server/adapters/express'; import express from 'express'; // created for each request const createContext = ({ req, res, }: trpcExpress.CreateExpressContextOptions) => ({}); // no context type Context = Awaited>; const t = initTRPC.context().create(); const appRouter = t.router({ // [...] }); const app = express(); app.use( '/trpc', trpcExpress.createExpressMiddleware({ router: appRouter, createContext, }), ); app.listen(4000); ``` Your endpoints are now available via HTTP! | Endpoint | HTTP URI | | ------------ | ---------------------------------------------------------------------------------------------------------- | | `getUser` | `GET http://localhost:4000/trpc/getUser?input=INPUT`

where `INPUT` is a URI-encoded JSON string. | | `createUser` | `POST http://localhost:4000/trpc/createUser`

with `req.body` of type `{name: string}` | -------------------------------------------------- --- id: fastify title: Fastify Adapter sidebar_label: Fastify slug: /server/adapters/fastify --- ## Example app The best way to start with the Fastify adapter is to take a look at the example application.
Description Links
  • Fastify server with WebSocket
  • Simple tRPC client in node
## How to use tRPC with Fastify ### Install dependencies ```bash yarn add @trpc/server fastify zod ``` > ⚠️ **Fastify version requirement** > > The tRPC v11 Fastify adapter requires **Fastify v5+**. > Using Fastify v4 may cause requests to return empty responses without errors. > [Zod](https://github.com/colinhacks/zod) isn't a required dependency, but it's used in the sample router below. :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: ### Create the router First of all you need a [router](/docs/server/routers) to handle your queries, mutations and subscriptions. A sample router is given below, save it in a file named `router.ts`.
router.ts ```ts twoslash title='router.ts' import { initTRPC } from '@trpc/server'; import { z } from 'zod'; type User = { id: string; name: string; bio?: string; }; const users: Record = {}; export const t = initTRPC.create(); export const appRouter = t.router({ getUserById: t.procedure.input(z.string()).query((opts) => { return users[opts.input]; // input type is string }), createUser: t.procedure .input( z.object({ name: z.string().min(3), bio: z.string().max(142).optional(), }), ) .mutation((opts) => { const id = Date.now().toString(); const user: User = { id, ...opts.input }; users[user.id] = user; return user; }), }); // export type definition of API export type AppRouter = typeof appRouter; ```
If your router file starts getting too big, split your router into several subrouters each implemented in its own file. Then [merge them](/docs/server/merging-routers) into a single root `appRouter`. ### Create the context Then you need a [context](/docs/server/context) that will be created for each request. A sample context is given below, save it in a file named `context.ts`:
context.ts ```ts twoslash title='context.ts' import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify'; export function createContext({ req, res }: CreateFastifyContextOptions) { const user = { name: req.headers.username ?? 'anonymous' }; return { req, res, user }; } export type Context = Awaited>; ```
### Create Fastify server tRPC includes an adapter for [Fastify](https://www.fastify.io/) out of the box. This adapter lets you convert your tRPC router into a [Fastify plugin](https://www.fastify.io/docs/latest/Reference/Plugins/). In order to prevent errors during large batch requests, make sure to set the `maxParamLength` Fastify option to a suitable value, as shown. :::tip Due to limitations in Fastify's plugin system and type inference, there might be some issues getting for example `onError` typed correctly. You can add a `satisfies FastifyTRPCPluginOptions['trpcOptions']` to help TypeScript out and get the correct types. ::: ```ts twoslash title='server.ts' // @types: node // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: context.ts import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify'; export function createContext({ req, res }: CreateFastifyContextOptions) { const user = { name: req.headers.username ?? 'anonymous' }; return { req, res, user }; } // @filename: server.ts // ---cut--- import { fastifyTRPCPlugin, FastifyTRPCPluginOptions, } from '@trpc/server/adapters/fastify'; import fastify from 'fastify'; import { createContext } from './context'; import { appRouter, type AppRouter } from './router'; const server = fastify({ routerOptions: { maxParamLength: 5000, }, }); server.register(fastifyTRPCPlugin, { prefix: '/trpc', trpcOptions: { router: appRouter, createContext, onError({ path, error }) { // report to error monitoring console.error(`Error in tRPC handler on path '${path}':`, error); }, } satisfies FastifyTRPCPluginOptions['trpcOptions'], }); (async () => { try { await server.listen({ port: 3000 }); } catch (err) { server.log.error(err); process.exit(1); } })(); ``` Your endpoints are now available via HTTP! | Endpoint | HTTP URI | | ------------- | -------------------------------------------------------------------------------------------------------------- | | `getUserById` | `GET http://localhost:3000/trpc/getUserById?input=INPUT`

where `INPUT` is a URI-encoded JSON string. | | `createUser` | `POST http://localhost:3000/trpc/createUser`

with `req.body` of type `User` | ## Enable WebSockets The Fastify adapter supports [WebSockets](../websockets.md) via the [@fastify/websocket](https://www.npmjs.com/package/@fastify/websocket) plugin. All you have to do in addition to the above steps is install the dependency, add some subscriptions to your router, and activate the `useWSS` [option](#fastify-plugin-options) in the plugin. The minimum `@fastify/websocket` version required is `3.11.0`. ### Install dependencies ```bash yarn add @fastify/websocket ``` ### Import and register `@fastify/websocket` ```ts twoslash // @filename: node_modules/@fastify/websocket/index.d.ts declare const plugin: any; export default plugin; // @filename: server.ts import fastify from 'fastify'; declare const server: ReturnType; // ---cut--- import ws from '@fastify/websocket'; server.register(ws); ``` ### Add some subscriptions Edit the `router.ts` file created in the previous steps and add the following code: ```ts twoslash title='router.ts' import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({ randomNumber: t.procedure.subscription(async function* () { while (true) { yield { randomNumber: Math.random() }; await new Promise((resolve) => setTimeout(resolve, 1000)); } }), }); ``` ### Activate the `useWSS` option ```ts twoslash title='server.ts' // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: context.ts import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify'; export function createContext({ req, res }: CreateFastifyContextOptions) { return { req, res }; } // @filename: server.ts // ---cut--- import { fastifyTRPCPlugin, FastifyTRPCPluginOptions, } from '@trpc/server/adapters/fastify'; import fastify from 'fastify'; import { createContext } from './context'; import { appRouter, type AppRouter } from './router'; const server = fastify(); server.register(fastifyTRPCPlugin, { useWSS: true, trpcOptions: { router: appRouter, createContext, // Enable heartbeat messages to keep connection open (disabled by default) keepAlive: { enabled: true, // server ping message interval in milliseconds pingMs: 30000, // connection is terminated if pong message is not received in this many milliseconds pongWaitMs: 5000, }, }, }); ``` You can now subscribe to the `randomNumber` topic and should receive a random number every second 🚀. ## Fastify plugin options | name | type | optional | default | description | | ----------- | -------------------------------------------------- | -------- | --------- | -------------------------------------------------------------- | | prefix | `string` | `true` | `"/trpc"` | URL prefix for tRPC routes | | useWSS | `boolean` | `true` | `false` | Enable WebSocket support via `@fastify/websocket` | | trpcOptions | `FastifyHandlerOptions` | `false` | `n/a` | tRPC handler options including `router`, `createContext`, etc. | -------------------------------------------------- --- id: fetch title: Fetch / Edge Runtimes Adapter sidebar_label: Fetch / Edge Runtimes slug: /server/adapters/fetch --- import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs'; You can create a tRPC server within any edge runtime that follow the [WinterCG](https://wintercg.org/), specifically the [Minimum Common Web Platform API](https://common-min-api.proposal.wintercg.org/) specification. Some of these runtimes include, but are not limited to: - Cloudflare Workers - Deno Deploy - Vercel Edge Runtime (& Next.js Edge Runtime) This also makes it easy to integrate into frameworks that use the web platform APIs to represent requests and responses, such as: - Astro (SSR mode) - Remix - SolidStart ## Example apps
Description Links
Cloudflare Workers example Source
Deno Deploy example Source
Next.js Edge Runtime example Source
Vercel Edge Runtime example Source
## How to use tRPC server with an edge runtime tRPC provides a [fetch adapter](/docs/server/adapters/fetch) that uses the native [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) APIs as input and output. The tRPC-specific code is the same across all runtimes, the only difference being how the response is returned. tRPC includes an adapter for the native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) out of the box. This adapter lets you convert your tRPC router into a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) handler that returns [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects. ## Required Web APIs tRPC server uses the following Fetch APIs: - `Request`, `Response` - `fetch` - `Headers` - `URL` If your runtime supports these APIs, you can [use tRPC server](#how-to-use-trpc-server-with-an-edge-runtime). :::tip Fun fact: that also means you can use a tRPC server in your browser! ::: ## Common setup ### Install dependencies :::tip You can skip this step if you use Deno Deploy. ::: import { InstallSnippet } from '@site/src/components/InstallSnippet'; > [Zod](https://github.com/colinhacks/zod) isn't a required dependency, but it's used in the sample router below. :::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: ### Create the router First of all you need a [router](/docs/server/routers) to handle your queries, mutations and subscriptions. A sample router is given below, save it in a file named `router.ts`.
router.ts ```ts twoslash title='router.ts' // @filename: context.ts import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'; export function createContext({ req, resHeaders }: FetchCreateContextFnOptions) { const user = { name: req.headers.get('username') ?? 'anonymous' }; return { req, resHeaders, user }; } export type Context = Awaited>; // @filename: router.ts // ---cut--- import { initTRPC } from '@trpc/server'; import { z } from 'zod'; import type { Context } from './context'; type User = { id: string; name: string; bio?: string; }; const users: Record = {}; export const t = initTRPC.context().create(); export const appRouter = t.router({ getUserById: t.procedure.input(z.string()).query((opts) => { return users[opts.input]; // input type is string }), createUser: t.procedure // validate input with Zod .input( z.object({ name: z.string().min(3), bio: z.string().max(142).optional(), }), ) .mutation((opts) => { const id = Date.now().toString(); const user: User = { id, ...opts.input }; users[user.id] = user; return user; }), }); // export type definition of API export type AppRouter = typeof appRouter; ```
If your router file starts getting too big, split your router into several subrouters each implemented in its own file. Then [merge them](/docs/server/merging-routers) into a single root `appRouter`. ### Create the context Then you need a [context](/docs/server/context) that will be created for each request. A sample context is given below, save it in a file named `context.ts`:
context.ts ```ts twoslash title='context.ts' import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'; export function createContext({ req, resHeaders, }: FetchCreateContextFnOptions) { const user = { name: req.headers.get('username') ?? 'anonymous' }; return { req, resHeaders, user }; } export type Context = Awaited>; ```
## Runtime-specific setup ### Astro ```ts twoslash title='src/pages/trpc/[trpc].ts' // @filename: astro.d.ts declare module 'astro' { export type APIRoute = (context: { request: Request }) => Response | Promise; } // @filename: src/server/context.ts import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'; export function createContext(opts: FetchCreateContextFnOptions) { return { user: { name: opts.req.headers.get('username') ?? 'anonymous' } }; } // @filename: src/server/router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: src/pages/trpc/[trpc].ts // ---cut--- import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import type { APIRoute } from 'astro'; import { createContext } from '../../server/context'; import { appRouter } from '../../server/router'; export const ALL: APIRoute = (opts) => { return fetchRequestHandler({ endpoint: '/trpc', req: opts.request, router: appRouter, createContext, }); }; ``` ### Cloudflare Worker :::note You need the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) to run Cloudflare Workers. ::: #### Create Cloudflare Worker ```ts twoslash title='server.ts' // @filename: context.ts import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'; export function createContext(opts: FetchCreateContextFnOptions) { return { user: { name: opts.req.headers.get('username') ?? 'anonymous' } }; } // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: server.ts // ---cut--- import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { createContext } from './context'; import { appRouter } from './router'; export default { async fetch(request: Request): Promise { return fetchRequestHandler({ endpoint: '/trpc', req: request, router: appRouter, createContext, }); }, }; ``` Run `wrangler dev server.ts` and your endpoints will be available via HTTP! | Endpoint | HTTP URI | | ------------ | -------------------------------------------------------------------------------------------------------------- | | `getUser` | `GET http://localhost:8787/trpc/getUserById?input=INPUT`

where `INPUT` is a URI-encoded JSON string. | | `createUser` | `POST http://localhost:8787/trpc/createUser`

with `req.body` of type `User` | ### Deno Oak :::note This assumes you have Deno installed and setup. Refer to their [getting started guide](https://deno.com/manual/getting_started/installation) for more information. ::: #### Update the imports in `router.ts` ```ts title='router.ts' import { initTRPC } from 'npm:@trpc/server'; import { z } from 'npm:zod'; import { Context } from './context.ts'; ``` #### Update the imports in `context.ts` ```ts title='context.ts' import { FetchCreateContextFnOptions } from 'npm:@trpc/server/adapters/fetch'; ``` #### Use `fetchRequestHandler` with Oak in `app.ts` ```ts title='app.ts' import { Application, Router } from 'https://deno.land/x/oak/mod.ts'; import { fetchRequestHandler } from 'npm:@trpc/server/adapters/fetch'; import { createContext } from './context.ts'; import { appRouter } from './router.ts'; const app = new Application(); const router = new Router(); router.all('/trpc/(.*)', async (ctx) => { const res = await fetchRequestHandler({ endpoint: '/trpc', req: new Request(ctx.request.url, { headers: ctx.request.headers, body: ctx.request.method !== 'GET' && ctx.request.method !== 'HEAD' ? ctx.request.body({ type: 'stream' }).value : void 0, method: ctx.request.method, }), router: appRouter, createContext, }); ctx.response.status = res.status; ctx.response.headers = res.headers; ctx.response.body = res.body; }); app.use(router.routes()); app.use(router.allowedMethods()); await app.listen({ port: 3000 }); ``` ### Deno Deploy :::note This assumes you have Deno installed and setup. Refer to their [getting started guide](https://deno.com/manual/getting_started/installation) for more information. ::: :::tip See our example [Deno Deploy app](https://github.com/trpc/trpc/tree/main/examples/deno-deploy) for a working example. ::: #### Update the imports in `router.ts` ```ts title='router.ts' import { initTRPC } from 'npm:@trpc/server'; import { z } from 'npm:zod'; import { Context } from './context.ts'; ``` #### Update the imports in `context.ts` ```ts title='context.ts' import { FetchCreateContextFnOptions } from 'npm:@trpc/server/adapters/fetch'; ``` #### Create Deno Deploy Function ```ts title='server.ts' import { fetchRequestHandler } from 'npm:@trpc/server/adapters/fetch'; import { createContext } from './context.ts'; import { appRouter } from './router.ts'; function handler(request) { return fetchRequestHandler({ endpoint: '/trpc', req: request, router: appRouter, createContext, }); } Deno.serve(handler); ``` Run `deno run --allow-net=:8000 --allow-env ./server.ts` and your endpoints will be available via HTTP! | Endpoint | HTTP URI | | ------------ | -------------------------------------------------------------------------------------------------------------- | | `getUser` | `GET http://localhost:8000/trpc/getUserById?input=INPUT`

where `INPUT` is a URI-encoded JSON string. | | `createUser` | `POST http://localhost:8000/trpc/createUser`

with `req.body` of type `User` | ### Next.js Edge Runtime See a full example [here](https://github.com/trpc/trpc/tree/main/examples/next-edge-runtime). ### Remix ```ts twoslash title='app/routes/trpc.$trpc.ts' // @filename: remix.d.ts declare module '@remix-run/node' { export type ActionFunctionArgs = { request: Request }; export type LoaderFunctionArgs = { request: Request }; } declare module '~/server/context' { import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'; export function createContext(opts: FetchCreateContextFnOptions): any; } declare module '~/server/router' { import type { AnyTRPCRouter } from '@trpc/server'; export const appRouter: AnyTRPCRouter; } // @filename: app/routes/trpc.$trpc.ts // ---cut--- import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node'; import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { createContext } from '~/server/context'; import { appRouter } from '~/server/router'; export const loader = async (args: LoaderFunctionArgs) => { return handleRequest(args); }; export const action = async (args: ActionFunctionArgs) => { return handleRequest(args); }; function handleRequest(args: LoaderFunctionArgs | ActionFunctionArgs) { return fetchRequestHandler({ endpoint: '/trpc', req: args.request, router: appRouter, createContext, }); } ``` ### SolidStart ```ts twoslash title='src/routes/api/trpc/[trpc].ts' // @filename: solidstart.d.ts declare module '@solidjs/start/server' { export type APIEvent = { request: Request }; } // @filename: src/routes/server/context.ts import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'; export function createContext(opts: FetchCreateContextFnOptions) { return { user: { name: opts.req.headers.get('username') ?? 'anonymous' } }; } // @filename: src/routes/server/router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: src/routes/api/trpc/[trpc].ts // ---cut--- import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import type { APIEvent } from '@solidjs/start/server'; import { createContext } from '../../server/context'; import { appRouter } from '../../server/router'; const handler = (event: APIEvent) => fetchRequestHandler({ endpoint: '/api/trpc', req: event.request, router: appRouter, createContext, }); export { handler as GET, handler as POST }; ``` ### Vercel Edge Runtime :::note See the official [Vercel Edge Runtime documentation](https://edge-runtime.vercel.app/getting-started) for more information. ::: :::tip See our example [Vercel Edge Runtime app](https://github.com/trpc/trpc/tree/main/examples/vercel-edge-runtime) for a working example. ::: #### Install dependencies ```sh npm install -g edge-runtime ``` ```sh yarn global add edge-runtime ``` ```sh pnpm add -g edge-runtime ``` ```sh bun add -g edge-runtime ``` #### Create Edge Runtime Function ```ts twoslash title='server.ts' // @filename: context.ts import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'; export function createContext(opts: FetchCreateContextFnOptions) { return { user: { name: opts.req.headers.get('username') ?? 'anonymous' } }; } // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: server.ts // ---cut--- import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { createContext } from './context'; import { appRouter } from './router'; // Vercel Edge Runtime uses Service Worker-style addEventListener addEventListener('fetch', (event: any) => { return event.respondWith( fetchRequestHandler({ endpoint: '/trpc', req: event.request, router: appRouter, createContext, }), ); }); ``` Run `edge-runtime --listen server.ts --port 3000` and your endpoints will be available via HTTP! | Endpoint | HTTP URI | | ------------ | -------------------------------------------------------------------------------------------------------------- | | `getUser` | `GET http://localhost:3000/trpc/getUserById?input=INPUT`

where `INPUT` is a URI-encoded JSON string. | | `createUser` | `POST http://localhost:3000/trpc/createUser`

with `req.body` of type `User` | -------------------------------------------------- --- id: nextjs title: Next.js Adapter sidebar_label: Next.js slug: /server/adapters/nextjs --- :::tip tRPC's support for Next.js is far more expansive than just an adapter. This page covers a brief summary of how to set up the adapter. For complete documentation, see the [Next.js Integration guide](../../client/nextjs/overview.mdx). ::: ## Example app
Description Links
Next.js Minimal Starter
## Next.js example Serving your tRPC router in a Next.js project is straight-forward. Just create an API handler in `pages/api/trpc/[trpc].ts` as shown below: ```ts twoslash title='pages/api/trpc/[trpc].ts' // @filename: server/trpc/context.ts import type { CreateNextContextOptions } from '@trpc/server/adapters/next'; export async function createContext(opts: CreateNextContextOptions) { return {}; } // @filename: server/trpc/router/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: pages/api/trpc/handler.ts // ---cut--- import { createNextApiHandler } from '@trpc/server/adapters/next'; import { createContext } from '../../../server/trpc/context'; import { appRouter } from '../../../server/trpc/router/_app'; export default createNextApiHandler({ router: appRouter, createContext, }); ``` ## Handling CORS and other advanced usage While you can usually just "set and forget" the API Handler as shown above, sometimes you might want to modify it further. The API handler created by `createNextApiHandler` and equivalents in other frameworks is just a function that takes `req` and `res` objects. This means you can also modify those objects before passing them to the handler, for example to [enable CORS](/docs/client/cors). ```ts twoslash title='pages/api/trpc/[trpc].ts' // @filename: server/trpc/context.ts import type { CreateNextContextOptions } from '@trpc/server/adapters/next'; export async function createContext(opts: CreateNextContextOptions) { return {}; } // @filename: server/trpc/router/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: pages/api/trpc/[trpc].ts // ---cut--- import type { NextApiRequest, NextApiResponse } from 'next'; import { createNextApiHandler } from '@trpc/server/adapters/next'; import { createContext } from '../../../server/trpc/context'; import { appRouter } from '../../../server/trpc/router/_app'; // create the API handler, but don't return it yet const nextApiHandler = createNextApiHandler({ router: appRouter, createContext, }); // https://nextjs.org/docs/api-routes/introduction export default async function handler( req: NextApiRequest, res: NextApiResponse, ) { // We can use the response object to enable CORS res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Request-Method', '*'); res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); res.setHeader('Access-Control-Allow-Headers', '*'); // If you need to make authenticated CORS calls then // remove what is above and uncomment the below code // Allow-Origin has to be set to the requesting domain that you want to send the credentials back to // res.setHeader('Access-Control-Allow-Origin', 'http://example:6006'); // res.setHeader('Access-Control-Request-Method', '*'); // res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); // res.setHeader('Access-Control-Allow-Headers', 'content-type'); // res.setHeader('Referrer-Policy', 'no-referrer'); // res.setHeader('Access-Control-Allow-Credentials', 'true'); if (req.method === 'OPTIONS') { res.writeHead(200); return res.end(); } // finally pass the request on to the tRPC handler return nextApiHandler(req, res); } ``` ## App Router (Route Handlers) If you're using the Next.js **App Router**, use the [fetch adapter](fetch) instead, as App Router route handlers are based on the Web standard `Request` and `Response` objects. See the [App Router setup guide](/docs/client/nextjs/app-router-setup) for a complete walkthrough. ```ts twoslash title='app/api/trpc/[trpc]/route.ts' // @filename: app/api/trpc/init.ts import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'; export function createTRPCContext(opts: FetchCreateContextFnOptions) { return {}; } // @filename: app/api/trpc/routers/_app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: app/api/trpc/[trpc]/route.ts // ---cut--- import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { createTRPCContext } from '../../trpc/init'; import { appRouter } from '../../trpc/routers/_app'; const handler = (req: Request) => fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext: createTRPCContext, }); export { handler as GET, handler as POST }; ``` -------------------------------------------------- --- id: standalone title: Standalone Adapter sidebar_label: Standalone slug: /server/adapters/standalone --- tRPC's Standalone Adapter is the simplest way to get a new project working. It's ideal for local development, and for server-based production environments. In essence it's just a wrapper around the standard [Node.js HTTP Server](https://nodejs.org/api/http.html) with the normal options related to tRPC. If you have an existing API deployment like [Express](express), [Fastify](fastify), or [Next.js](nextjs), which you want to integrate tRPC into, you should have a look at their respective adapters. Likewise if you have a preference to host on serverless or edge compute, we have adapters like [AWS Lambda](aws-lambda) and [Fetch](fetch) which may fit your needs. It's also not uncommon, where the deployed adapter is hard to run on local machines, to have 2 entry-points in your application. You could use the Standalone Adapter for local development, and a different adapter when deployed. ## Example app
Description Links
Standalone tRPC Server
Standalone tRPC Server with CORS handling
:::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation: ```bash npx @tanstack/intent@latest install ``` ::: ## Setting up a Standalone tRPC Server ### 1. Implement your App Router Implement your tRPC router. For example: ```ts twoslash title='appRouter.ts' import { initTRPC } from '@trpc/server'; import { z } from 'zod'; export const t = initTRPC.create(); export const appRouter = t.router({ getUser: t.procedure.input(z.string()).query((opts) => { return { id: opts.input, name: 'Bilbo' }; }), createUser: t.procedure .input(z.object({ name: z.string().min(5) })) .mutation(async (opts) => { // use your ORM of choice return { id: '1', ...opts.input }; }), }); // export type definition of API export type AppRouter = typeof appRouter; ``` For more information, you can look at the [quickstart guide](/docs/quickstart) ### 2. Use the Standalone adapter The Standalone adapter runs a simple Node.js HTTP server. ```ts twoslash title='server.ts' // @filename: appRouter.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: server.ts // ---cut--- import { createHTTPServer } from '@trpc/server/adapters/standalone'; import { appRouter } from './appRouter'; createHTTPServer({ router: appRouter, createContext() { return {}; }, // basePath: '/trpc/', // optional, defaults to '/' }).listen(2022); ``` ## Handling CORS & OPTIONS By default the standalone server will not respond to HTTP OPTIONS requests, or set any CORS headers. If you're not hosting in an environment which can handle this for you, like during local development, you may need to handle it. ### 1. Install cors You can add support yourself with the popular `cors` package ```bash yarn add cors yarn add -D @types/cors ``` For full information on how to configure this package, [check the docs](https://github.com/expressjs/cors#readme) ### 2. Configure the Standalone server This example just throws open CORS to any request, which is useful for development, but you can and should configure it more strictly in a production environment. ```ts twoslash title='server.ts' // @filename: appRouter.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: server.ts // ---cut--- import { createHTTPServer } from '@trpc/server/adapters/standalone'; import cors from 'cors'; import { appRouter } from './appRouter'; createHTTPServer({ middleware: cors(), router: appRouter, createContext() { return {}; }, }).listen(3333); ``` The `middleware` option will accept any function which resembles a connect/node.js middleware, so it can be used for more than `cors` handling if you wish. It is, however, intended to be a simple escape hatch and as such won't on its own allow you to compose multiple middlewares together. If you want to do this then you could: 1. Use an alternate adapter with more comprehensive middleware support, like the [Express adapter](/docs/server/adapters/express) 2. Use a solution to compose middlewares such as [connect](https://github.com/senchalabs/connect) 3. Extend the Standalone `createHTTPHandler` with a custom http server (see below) ## Adding a handler to a Custom HTTP server `createHTTPServer` is returning an instance of Node's built-in [`http.Server`](https://nodejs.org/api/http.html#class-httpserver), which means that you have an access to all its properties and APIs. However, if `createHTTPServer` isn't enough for your use case, you can also use the standalone adapter's `createHTTPHandler` function to create your own HTTP server. For instance: ```ts twoslash title='server.ts' // @types: node // @filename: appRouter.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: server.ts // ---cut--- import { createServer } from 'http'; import { createHTTPHandler } from '@trpc/server/adapters/standalone'; import { appRouter } from './appRouter'; const handler = createHTTPHandler({ router: appRouter, createContext() { return {}; }, }); createServer((req, res) => { /** * Handle the request however you like, * just call the tRPC handler when you're ready */ handler(req, res); }).listen(3001); ``` ## Custom base path for handling requests {#custom-basePath} The Standalone adapter also supports a `basePath` option, which will slice the basePath from the beginning of the request path. ```ts twoslash title='server.ts' // @types: node // @filename: appRouter.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: server.ts // ---cut--- import { createServer } from 'http'; import { createHTTPHandler } from '@trpc/server/adapters/standalone'; import { appRouter } from './appRouter'; const handler = createHTTPHandler({ router: appRouter, basePath: '/trpc/', }); createServer((req, res) => { if (req.url?.startsWith('/trpc/')) { return handler(req, res); } // [... insert your custom logic here ...] res.statusCode = 404; res.end('Not Found'); }).listen(3001); ``` ## HTTP2 The Standalone adapter also supports HTTP/2. ```ts twoslash title='server.ts' // @types: node // @filename: _app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: context.ts import type { CreateHTTP2ContextOptions } from '@trpc/server/adapters/standalone'; export async function createContext(opts: CreateHTTP2ContextOptions) { return {}; } // @filename: server.ts // ---cut--- import http2 from 'http2'; import { createHTTP2Handler } from '@trpc/server/adapters/standalone'; import { appRouter } from './_app'; import { createContext } from './context'; const handler = createHTTP2Handler({ router: appRouter, createContext, // basePath: '/trpc/', // optional, defaults to '/' }); const server = http2.createSecureServer( { key: '...', cert: '...', }, (req, res) => { /** * Handle the request however you like, * just call the tRPC handler when you're ready */ handler(req, res); }, ); server.listen(3001); ``` ```ts twoslash title='context.ts' import { CreateHTTP2ContextOptions } from '@trpc/server/adapters/standalone'; export async function createContext(opts: CreateHTTP2ContextOptions) { opts.req; // ^? opts.res; // ^? opts.info; // ^? return {}; } export type Context = Awaited>; ``` -------------------------------------------------- --- id: adapters title: Overview sidebar_label: Overview slug: /server/adapters --- tRPC is not a server on its own, and must therefore be served using other hosts, such as a simple [Node.js HTTP Server](adapters/standalone), [Express](adapters/express), or even [Next.js](adapters/nextjs). Most tRPC features are the same no matter which backend you choose. **Adapters** act as the glue between the host system and your tRPC API. Adapters typically follow some common conventions, allowing you to set up context creation via `createContext`, and globally handle errors via `onError`, but importantly allow you to choose an appropriate host for your application. We support many modes of hosting an API, which you will find documented here. - For serverful APIs, you might want our [Standalone](adapters/standalone) adapter, or use the [Express](adapters/express) or [Fastify](adapters/fastify) adapters to hook into your existing APIs - You might want a serverless solution and choose [AWS Lambda](adapters/aws-lambda), or [Fetch](adapters/fetch) for edge runtimes - You might have a full-stack framework and want a full integration like [Next.js](adapters/nextjs), or you could use the [Fetch](adapters/fetch) adapter with Next.js, Astro, Remix, or SolidStart :::tip For local development or serverful infrastructure, the simplest Adapter to use is the [Standalone Adapter](adapters/standalone), which can be used to run a standard Node.js HTTP Server. We recommend this when you need to get started quickly and have no existing HTTP Server to integrate with. Swapping out later is trivial if your needs change. ::: -------------------------------------------------- --- id: authorization title: Authorization sidebar_label: Authorization slug: /server/authorization --- The `createContext` function is called for each incoming request, so here you can add contextual information about the calling user from the request object. ## Create context from request headers ```ts twoslash title='server/context.ts' // @filename: utils.ts export async function decodeAndVerifyJwtToken(token: string) { return { name: 'user' }; } // @filename: context.ts // ---cut--- import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'; import { decodeAndVerifyJwtToken } from './utils'; export async function createContext({ req, res }: CreateHTTPContextOptions) { // Create your context based on the request object // Will be available as `ctx` in all your resolvers // This is just an example of something you might want to do in your ctx fn async function getUserFromHeader() { if (req.headers.authorization) { const user = await decodeAndVerifyJwtToken( req.headers.authorization.split(' ')[1], ); return user; } return null; } const user = await getUserFromHeader(); return { user, }; } export type Context = Awaited>; ``` ## Option 1: Authorize using resolver ```ts twoslash title='server/routers/_app.ts' import { initTRPC, TRPCError } from '@trpc/server'; import { z } from 'zod'; type Context = { user: { name: string } | null }; export const t = initTRPC.context().create(); const appRouter = t.router({ // open for anyone hello: t.procedure .input(z.string().nullish()) .query((opts) => `hello ${opts.input ?? opts.ctx.user?.name ?? 'world'}`), // checked in resolver secret: t.procedure.query((opts) => { if (!opts.ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return { secret: 'sauce', }; }), }); ``` ## Option 2: Authorize using middleware ```ts twoslash title='server/routers/_app.ts' import { initTRPC, TRPCError } from '@trpc/server'; import { z } from 'zod'; type Context = { user: { name: string } | null }; export const t = initTRPC.context().create(); // you can reuse this for any procedure export const protectedProcedure = t.procedure.use( async function isAuthed(opts) { const { ctx } = opts; // `ctx.user` is nullable if (!ctx.user) { // ^? throw new TRPCError({ code: 'UNAUTHORIZED' }); } return opts.next({ ctx: { // ✅ user value is known to be non-null now user: ctx.user, // ^? }, }); }, ); t.router({ // this is accessible for everyone hello: t.procedure .input(z.string().nullish()) .query((opts) => `hello ${opts.input ?? opts.ctx.user?.name ?? 'world'}`), admin: t.router({ // this is accessible only to admins secret: protectedProcedure.query((opts) => { return { secret: 'sauce', }; }), }), }); ``` -------------------------------------------------- --- id: caching title: Response Caching sidebar_label: Response Caching slug: /server/caching --- Since all tRPC queries are normal HTTP `GET` requests, you can use standard HTTP cache headers to cache responses. This can make responses snappy, give your database a rest, and help scale your API. :::info Always be careful with caching - especially if you handle personal information.   Since batching is enabled by default, it's recommended to set your cache headers in the `responseMeta` function and make sure that there are not any concurrent calls that may include personal data - or to omit cache headers completely if there is an auth header or cookie.   You can also use a [`splitLink`](../client/links/splitLink.mdx) to split your public requests and those that should be private and uncached. ::: ## Using `responseMeta` to cache responses Most tRPC adapters support a `responseMeta` callback that lets you set HTTP headers (including cache headers) based on the procedures being called. > This works with any hosting provider that supports standard HTTP cache headers (e.g. Vercel, Cloudflare, AWS CloudFront). ```ts twoslash title='server.ts' import { initTRPC } from '@trpc/server'; import { createHTTPServer } from '@trpc/server/adapters/standalone'; import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'; export const createContext = async (opts: CreateHTTPContextOptions) => { return { req: opts.req, res: opts.res, }; }; type Context = Awaited>; export const t = initTRPC.context().create(); const waitFor = async (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export const appRouter = t.router({ public: t.router({ slowQueryCached: t.procedure.query(async (opts) => { await waitFor(5000); // wait for 5s return { lastUpdated: new Date().toJSON(), }; }), }), }); // Exporting `type AppRouter` only exposes types that can be used for inference // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export export type AppRouter = typeof appRouter; // export API handler const server = createHTTPServer({ router: appRouter, createContext, responseMeta(opts) { const { paths, errors, type } = opts; // assuming you have all your public routes with the keyword `public` in them const allPublic = paths && paths.every((path) => path.includes('public')); // checking that no procedures errored const allOk = errors.length === 0; // checking we're doing a query request const isQuery = type === 'query'; if (allPublic && allOk && isQuery) { // cache request for 1 day + revalidate once every second const ONE_DAY_IN_SECONDS = 60 * 60 * 24; return { headers: new Headers([ [ 'cache-control', `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`, ], ]), }; } return {}; }, }); server.listen(3000); ``` :::tip If you are using Next.js, see the [Next.js SSR caching guide](/docs/client/nextjs/pages-router/ssr#response-caching-with-ssr) for Next.js-specific caching examples using `createTRPCNext` and the Next.js adapter. ::: -------------------------------------------------- --- id: context title: Context sidebar_label: Context slug: /server/context --- Your context holds data that all of your tRPC procedures will have access to, and is a great place to put things like authentication information. Setting up the context is done in 2 steps, defining the type during initialization and then creating the runtime context for each request. ## Defining the context type When initializing tRPC using `initTRPC`, you should pipe `.context()` to the `initTRPC` builder function before calling `.create()`. The type `TContext` can either be inferred from a function's return type or be explicitly defined. This will make sure your context is properly typed in your procedures and middlewares. ```ts twoslash import * as trpc from '@trpc/server'; // ---cut--- import { initTRPC } from '@trpc/server'; import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'; export const createContext = async (opts: CreateHTTPContextOptions) => { // Example: extract a session token from the request headers const token = opts.req.headers['authorization']; return { token, }; }; export type Context = Awaited>; const t = initTRPC.context().create(); t.procedure.use((opts) => { opts.ctx; // ^? return opts.next(); }); ``` ## Creating the context The `createContext()` function must be passed to the handler mounting your appRouter. The handler may use HTTP or a [server-side call](server-side-calls). `createContext()` is called once per request, so all procedures within a single batched request share the same context. ```ts twoslash // @filename: context.ts import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'; export async function createContext(opts: CreateHTTPContextOptions) { return { token: opts.req.headers['authorization'] }; } export type Context = Awaited>; // @filename: router.ts import { initTRPC } from '@trpc/server'; import type { Context } from './context'; const t = initTRPC.context().create(); export const appRouter = t.router({}); // @filename: server.ts // ---cut--- // 1. HTTP request import { createHTTPHandler } from '@trpc/server/adapters/standalone'; import { createContext } from './context'; import { appRouter } from './router'; const handler = createHTTPHandler({ router: appRouter, createContext, }); ``` ```ts twoslash // @target: esnext // @filename: context.ts export async function createContext() { return { token: 'test' }; } // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export const createCaller = t.createCallerFactory(appRouter); // @filename: call.ts // ---cut--- // 2. Server-side call import { createContext } from './context'; import { createCaller } from './router'; const caller = createCaller(await createContext()); ``` ```ts twoslash // @target: esnext // @filename: context.ts export async function createContext() { return {}; } // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: helpers.ts // ---cut--- // 3. Server-side helpers (Next.js-specific, see /docs/client/nextjs/pages-router/server-side-helpers) import { createServerSideHelpers } from '@trpc/react-query/server'; import { createContext } from './context'; import { appRouter } from './router'; const helpers = createServerSideHelpers({ router: appRouter, ctx: await createContext(), }); ``` ## Example code ```ts twoslash // ------------------------------------------------- // @filename: context.ts // ------------------------------------------------- import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'; /** * Creates context for an incoming request * @see https://trpc.io/docs/v11/context */ export async function createContext(opts: CreateHTTPContextOptions) { const token = opts.req.headers['authorization']; // In a real app, you would verify the token and look up the user const user = token ? { email: 'user@example.com' } : null; return { user, }; } export type Context = Awaited>; // ------------------------------------------------- // @filename: trpc.ts // ------------------------------------------------- import { initTRPC, TRPCError } from '@trpc/server'; import { Context } from './context'; const t = initTRPC.context().create(); export const router = t.router; /** * Unprotected procedure */ export const publicProcedure = t.procedure; /** * Protected procedure */ export const protectedProcedure = t.procedure.use(function isAuthed(opts) { if (!opts.ctx.user?.email) { throw new TRPCError({ code: 'UNAUTHORIZED', }); } return opts.next({ ctx: { // Infers the `user` as non-nullable user: opts.ctx.user, }, }); }); ``` ## Inner and outer context In some scenarios it could make sense to split up your context into "inner" and "outer" functions. **Inner context** is where you define context which doesn’t depend on the request, e.g. your database connection. You can use this function for integration testing or [server-side calls](/docs/server/server-side-calls), where you don’t have a request object. Whatever is defined here will **always** be available in your procedures. :::note Tradeoff for large clients in `createContextInner` Putting a database client such as `prisma` on `createContextInner` is convenient and common, but large generated clients (like Prisma) can increase type-checking overhead because they become part of your context type across procedures. If that overhead becomes noticeable, an alternative is to keep context smaller and import the client directly at call sites where needed. ::: **Outer context** is where you define context which depends on the request, e.g. for the user's session. Whatever is defined here is only available for procedures that are called via HTTP. ### Example for inner & outer context ```ts twoslash // @types: node // @filename: auth.ts import type { IncomingMessage } from 'http'; export type Session = { user: { email: string } }; export function getSessionFromCookie(req: IncomingMessage): Session | null { return null; } // @filename: db.ts export const db = {}; // @filename: context.ts // ---cut--- import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone'; import { getSessionFromCookie, type Session } from './auth'; import { db } from './db'; /** * Defines your inner context shape. * Add fields here that the inner context brings. */ interface CreateInnerContextOptions { session: Session | null; } /** * Inner context. Will always be available in your procedures, in contrast to the outer context. * * Also useful for: * - testing, so you don't have to mock `req`/`res` * - server-side calls where we don't have `req`/`res` * * @see https://trpc.io/docs/v11/context#inner-and-outer-context */ export async function createContextInner(opts?: CreateInnerContextOptions) { return { db, session: opts?.session, }; } /** * Outer context. Used in the routers and will e.g. bring `req` & `res` to the context as "not `undefined`". * * @see https://trpc.io/docs/v11/context#inner-and-outer-context */ export async function createContext(opts: CreateHTTPContextOptions) { const session = getSessionFromCookie(opts.req); const contextInner = await createContextInner({ session }); return { ...contextInner, req: opts.req, res: opts.res, }; } export type Context = Awaited>; // The usage in your router is the same as the example above. ``` It is important to infer your `Context` from the **inner** context, as only what is defined there is really always available in your procedures. If you don't want to check `req` or `res` for `undefined` in your procedures all the time, you could build a small reusable procedure for that: ```ts twoslash // @types: node import type { IncomingMessage, ServerResponse } from 'http'; import { initTRPC } from '@trpc/server'; type Context = { req: IncomingMessage | undefined; res: ServerResponse | undefined; }; const t = initTRPC.context().create(); const publicProcedure = t.procedure; // ---cut--- export const apiProcedure = publicProcedure.use((opts) => { if (!opts.ctx.req || !opts.ctx.res) { throw new Error('You are missing `req` or `res` in your call.'); } return opts.next({ ctx: { // We overwrite the context with the truthy `req` & `res`, which will also overwrite the types used in your procedure. req: opts.ctx.req, res: opts.ctx.res, }, }); }); ``` -------------------------------------------------- --- id: data-transformers title: Data Transformers sidebar_label: Data Transformers slug: /server/data-transformers --- You are able to serialize the response data & input args. The transformers need to be added both to the server and the client. ## Using [superjson](https://github.com/blitz-js/superjson) SuperJSON allows us to transparently use, e.g., standard `Date`/`Map`/`Set`s over the wire between the server and client. That is, you can return any of these types from your API-resolver and use them in the client without having to recreate the objects from JSON. ### How to #### 1. Install ```bash yarn add superjson ``` #### 2. Add to your `initTRPC` ```ts twoslash title='server/routers/_app.ts' import { initTRPC } from '@trpc/server'; import superjson from 'superjson'; export const t = initTRPC.create({ transformer: superjson, }); ``` #### 3. Add to `httpLink()`, `wsLink()`, etc > TypeScript will guide you to where you need to add `transformer` as soon as you've added it on the `initTRPC`-object `createTRPCClient()`: ```ts twoslash title='src/app/_trpc/client.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; import superjson from 'superjson'; const t = initTRPC.create({ transformer: superjson }); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpLink } from '@trpc/client'; import type { AppRouter } from './server'; import superjson from 'superjson'; export const client = createTRPCClient({ links: [ httpLink({ url: 'http://localhost:3000', transformer: superjson, }), ], }); ``` ## Using [devalue](https://github.com/Rich-Harris/devalue) Devalue works like superjson, focusing on performance and compact payloads, but at the cost of a less human-readable body. ### How to #### 1. Install ```bash yarn add devalue ``` #### 2. Add to `utils/trpc.ts` Here we use `parse` and `stringify` as they [mitigate XSS](https://github.com/Rich-Harris/devalue?tab=readme-ov-file#xss-mitigation). ```ts twoslash title='utils/trpc.ts' // @filename: devalue.d.ts declare module 'devalue' { export function parse(str: string): any; export function stringify(value: any): string; } // @filename: utils/trpc.ts // ---cut--- import { parse, stringify } from 'devalue'; // [...] export const transformer = { deserialize: (object: any) => parse(object), serialize: (object: any) => stringify(object), }; ``` #### 3. Add to your `initTRPC` ```ts twoslash title='server/routers/_app.ts' // @filename: utils/trpc.ts export const transformer = { deserialize: (object: any) => object, serialize: (object: any) => object, }; // @filename: server/routers/_app.ts // ---cut--- import { initTRPC } from '@trpc/server'; import { transformer } from '../../utils/trpc'; export const t = initTRPC.create({ transformer, }); ``` #### 4. Add to `httpLink()`, `wsLink()`, etc > TypeScript will guide you to where you need to add `transformer` as soon as you've added it on the `initTRPC`-object `createTRPCClient()`: ```ts twoslash title='src/app/_trpc/client.ts' // @filename: utils/trpc.ts export const transformer = { deserialize: (object: any) => object, serialize: (object: any) => object, }; // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; import { transformer } from '../../utils/trpc'; const t = initTRPC.create({ transformer }); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpLink } from '@trpc/client'; import type { AppRouter } from './server/routers/_app'; import { transformer } from './utils/trpc'; export const client = createTRPCClient({ links: [ httpLink({ url: 'http://localhost:3000', transformer, }), ], }); ``` ## Different transformers for upload and download If a transformer should only be used for one direction or different transformers should be used for upload and download (e.g., for performance reasons), you can provide individual transformers for upload and download. Make sure you use the same combined transformer everywhere. ## `DataTransformer` interface ```ts twoslash export interface DataTransformer { serialize(object: any): any; deserialize(object: any): any; } interface InputDataTransformer extends DataTransformer { /** * This function runs **on the client** before sending the data to the server. */ serialize(object: any): any; /** * This function runs **on the server** to transform the data before it is passed to the resolver */ deserialize(object: any): any; } interface OutputDataTransformer extends DataTransformer { /** * This function runs **on the server** before sending the data to the client. */ serialize(object: any): any; /** * This function runs **only on the client** to transform the data sent from the server. */ deserialize(object: any): any; } export interface CombinedDataTransformer { /** * Specify how the data sent from the client to the server should be transformed. */ input: InputDataTransformer; /** * Specify how the data sent from the server to the client should be transformed. */ output: OutputDataTransformer; } ``` -------------------------------------------------- --- id: error-formatting title: Error Formatting sidebar_label: Error Formatting slug: /server/error-formatting --- The error formatting in your router will be inferred all the way to your client. ## Usage example highlighted ### Adding custom formatting ```ts twoslash title='server.ts' import { initTRPC } from '@trpc/server'; import { ZodError } from 'zod'; export const t = initTRPC.create({ errorFormatter(opts) { const { shape, error } = opts; return { ...shape, data: { ...shape.data, zodError: error.code === 'BAD_REQUEST' && error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, }); ``` ### Usage in React ```tsx twoslash title='components/MyComponent.tsx' // @jsx: react-jsx // @filename: server.ts import { initTRPC } from '@trpc/server'; import { ZodError } from 'zod'; import { z } from 'zod'; const t = initTRPC.create({ errorFormatter(opts) { const { shape, error } = opts; return { ...shape, data: { ...shape.data, zodError: error.code === 'BAD_REQUEST' && error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, }); export const appRouter = t.router({ addPost: t.procedure.input(z.object({ title: z.string() })).mutation(({ input }) => input), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); // @filename: components/MyComponent.tsx // ---cut--- import { useEffect } from 'react'; import { trpc } from '../utils/trpc'; export function MyComponent() { const mutation = trpc.addPost.useMutation(); useEffect(() => { mutation.mutate({ title: 'example' }); }, []); if (mutation.error?.data?.zodError) { // zodError will be inferred return (
Error: {JSON.stringify(mutation.error.data.zodError, null, 2)}
); } return <>[...]; } ``` ## All properties sent to `errorFormatter()` > tRPC is compliant with [JSON-RPC 2.0](https://www.jsonrpc.org/specification) ```ts twoslash import { TRPCError } from '@trpc/server'; // ---cut--- interface ErrorFormatterOpts { error: TRPCError; type: 'query' | 'mutation' | 'subscription' | 'unknown'; path: string | undefined; input: unknown; ctx: unknown; shape: { message: string; code: number; data: unknown }; } ``` **`DefaultErrorShape`:** ```ts twoslash import type { TRPC_ERROR_CODE_KEY, TRPC_ERROR_CODE_NUMBER } from '@trpc/server'; // ---cut--- type DefaultErrorData = { code: TRPC_ERROR_CODE_KEY; httpStatus: number; /** * Path to the procedure that threw the error */ path?: string; /** * Stack trace of the error (only in development) */ stack?: string; }; interface DefaultErrorShape { message: string; code: TRPC_ERROR_CODE_NUMBER; data: DefaultErrorData; } ``` -------------------------------------------------- --- id: error-handling title: Error Handling sidebar_label: Error Handling slug: /server/error-handling --- Whenever an error occurs in a procedure, tRPC responds to the client with an object that includes an "error" property. This property contains all the information that you need to handle the error in the client. Here's an example error response caused by a bad request input: ```json { "id": null, "error": { "message": "\"password\" must be at least 4 characters", "code": -32600, "data": { "code": "BAD_REQUEST", "httpStatus": 400, "stack": "...", "path": "user.changepassword" } } } ``` ## Stack traces in production By default, tRPC includes `error.data.stack` only when [`isDev`](routers#initialize-trpc) is `true`. `initTRPC.create()` sets `isDev` to `process.env.NODE_ENV !== 'production'` by default. If you need deterministic behavior across runtimes, override `isDev` manually. ```ts twoslash title='server.ts' import { initTRPC } from '@trpc/server'; const t = initTRPC.create({ isDev: false }); ``` If you need stricter control over which error fields are returned, use [error formatting](error-formatting). ## Error codes tRPC defines a list of error codes that each represent a different type of error and response with a different HTTP code. | Code | Description | HTTP code | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | | PARSE_ERROR | Invalid JSON was received by the server, or an error occurred while parsing the request. | 400 | | BAD_REQUEST | The server cannot or will not process the request due to something that is perceived to be a client error. | 400 | | UNAUTHORIZED | The client request has not been completed because it lacks valid authentication credentials for the requested resource. | 401 | | PAYMENT_REQUIRED | The client request requires payment to access the requested resource. | 402 | | FORBIDDEN | The client is not authorized to access the requested resource. | 403 | | NOT_FOUND | The server cannot find the requested resource. | 404 | | METHOD_NOT_SUPPORTED | The server knows the request method, but the target resource doesn't support this method. | 405 | | TIMEOUT | The server would like to shut down this unused connection. | 408 | | CONFLICT | The request conflicts with the current state of the target resource. | 409 | | PRECONDITION_FAILED | Access to the target resource has been denied. | 412 | | PAYLOAD_TOO_LARGE | Request entity is larger than limits defined by server. | 413 | | UNSUPPORTED_MEDIA_TYPE | The server refuses to accept the request because the payload format is in an unsupported format. | 415 | | UNPROCESSABLE_CONTENT | The server understands the request method, and the request entity is correct, but the server was unable to process it. | 422 | | PRECONDITION_REQUIRED | [The server cannot process the request because a required precondition header (such as `If-Match`) is missing. When a precondition header does not match the server-side state, the response should be `412 Precondition Failed`.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428) | 428 | | TOO_MANY_REQUESTS | The rate limit has been exceeded or too many requests are being sent to the server. | 429 | | CLIENT_CLOSED_REQUEST | The client closed the connection before the server finished responding. | 499 | | INTERNAL_SERVER_ERROR | An unspecified error occurred. | 500 | | NOT_IMPLEMENTED | The server does not support the functionality required to fulfill the request. | 501 | | BAD_GATEWAY | The server received an invalid response from the upstream server. | 502 | | SERVICE_UNAVAILABLE | The server is not ready to handle the request. | 503 | | GATEWAY_TIMEOUT | The server did not get a response in time from the upstream server that it needed in order to complete the request. | 504 | tRPC exposes a helper function, `getHTTPStatusCodeFromError`, to help you extract the HTTP code from the error: ```ts twoslash import { TRPCError } from '@trpc/server'; // ---cut--- import { getHTTPStatusCodeFromError } from '@trpc/server/http'; // Example error you might get if your input validation fails const error: TRPCError = { name: 'TRPCError', code: 'BAD_REQUEST', message: '"password" must be at least 4 characters', }; if (error instanceof TRPCError) { const httpCode = getHTTPStatusCodeFromError(error); console.log(httpCode); // 400 } ``` :::tip There's a full example of how error handling works in a server-side context in the [Server Side Calls docs](server-side-calls). ::: ## Throwing errors tRPC provides an error subclass, `TRPCError`, which you can use to represent an error that occurred inside a procedure. For example, throwing this error: ```ts twoslash title='server.ts' import { initTRPC, TRPCError } from '@trpc/server'; const t = initTRPC.create(); const theError = new Error('something went wrong'); const appRouter = t.router({ hello: t.procedure.query(() => { throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'An unexpected error occurred, please try again later.', // optional: pass the original error to retain stack trace cause: theError, }); }), }); // [...] ``` Results in the following response: ```json { "id": null, "error": { "message": "An unexpected error occurred, please try again later.", "code": -32603, "data": { "code": "INTERNAL_SERVER_ERROR", "httpStatus": 500, "stack": "...", "path": "hello" } } } ``` ## Handling errors All errors that occur in a procedure go through the `onError` method before being sent to the client. Here you can handle errors (To change errors see [error formatting](error-formatting)). ```ts twoslash title='server.ts' // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: server.ts // ---cut--- import { createHTTPServer } from '@trpc/server/adapters/standalone'; import { appRouter } from './router'; const server = createHTTPServer({ router: appRouter, onError(opts) { const { error, type, path, input, ctx, req } = opts; console.error('Error:', error); if (error.code === 'INTERNAL_SERVER_ERROR') { // send to bug reporting } }, }); ``` The `onError` parameter is an object that contains all information about the error and the context it occurs in: ```ts twoslash import { TRPCError } from '@trpc/server'; // ---cut--- interface OnErrorOpts { error: TRPCError; type: 'query' | 'mutation' | 'subscription' | 'unknown'; path: string | undefined; input: unknown; ctx: unknown; req: Request; } ``` -------------------------------------------------- --- id: merging-routers title: Merging Routers sidebar_label: Merging Routers slug: /server/merging-routers --- Writing all API-code in the same file can become unwieldy. It's easy to merge routers together in order to break them up. ## Merging with child routers ```twoslash include trpcbase import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const router = t.router; export const publicProcedure = t.procedure; export const mergeRouters = t.mergeRouters; ``` ```ts twoslash title='routers/user.ts' // @filename: trpc.ts // @include: trpcbase // @filename: routers/user.ts // ---cut--- import { router, publicProcedure } from '../trpc'; import { z } from 'zod'; export const userRouter = router({ list: publicProcedure.query(() => { // [..] return []; }), }); ``` ```ts twoslash title='routers/post.ts' // @filename: trpc.ts // @include: trpcbase // @filename: routers/post.ts // ---cut--- import { router, publicProcedure } from '../trpc'; import { z } from 'zod'; export const postRouter = router({ create: publicProcedure .input( z.object({ title: z.string(), }), ) .mutation((opts) => { const { input } = opts; // [...] }), list: publicProcedure.query(() => { // ... return []; }), }); ``` ```ts twoslash title='routers/_app.ts' // @filename: trpc.ts // @include: trpcbase // @filename: routers/user.ts import { router, publicProcedure } from '../trpc'; import { z } from 'zod'; export const userRouter = router({ list: publicProcedure.query(() => { return []; }) }); // @filename: routers/post.ts import { router, publicProcedure } from '../trpc'; import { z } from 'zod'; export const postRouter = router({ create: publicProcedure.input(z.object({ title: z.string() })).mutation((opts) => {}), list: publicProcedure.query(() => { return []; }) }); // @filename: routers/_app.ts // ---cut--- import { router } from '../trpc'; import { userRouter } from './user'; import { postRouter } from './post'; const appRouter = router({ user: userRouter, post: postRouter, }); appRouter.user // ^? appRouter.post // ^? export type AppRouter = typeof appRouter; ``` ## Merging with `t.mergeRouters` If you prefer having all procedures flat in one single namespace, you can instead use `t.mergeRouters` ```ts twoslash title='routers/user.ts' // @filename: trpc.ts // @include: trpcbase // @filename: routers/user.ts // ---cut--- import { router, publicProcedure } from '../trpc'; import { z } from 'zod'; export const userRouter = router({ userList: publicProcedure.query(() => { // [..] return []; }), }); ``` ```ts twoslash title='routers/post.ts' // @filename: trpc.ts // @include: trpcbase // @filename: routers/post.ts // ---cut--- import { router, publicProcedure } from '../trpc'; import { z } from 'zod'; export const postRouter = router({ postCreate: publicProcedure .input( z.object({ title: z.string(), }), ) .mutation((opts) => { const { input } = opts; // [...] }), postList: publicProcedure.query(() => { // ... return []; }), }); ``` ```ts twoslash title='routers/_app.ts' // @filename: trpc.ts // @include: trpcbase // @filename: routers/user.ts import { router, publicProcedure } from '../trpc'; import { z } from 'zod'; export const userRouter = router({ userList: publicProcedure.query(() => { return []; }) }); // @filename: routers/post.ts import { router, publicProcedure } from '../trpc'; import { z } from 'zod'; export const postRouter = router({ postCreate: publicProcedure.input(z.object({ title: z.string() })).mutation((opts) => {}), postList: publicProcedure.query(() => { return []; }) }); // @filename: routers/_app.ts // ---cut--- import { mergeRouters } from '../trpc'; import { userRouter } from './user'; import { postRouter } from './post'; const appRouter = mergeRouters(userRouter, postRouter); // ^? export type AppRouter = typeof appRouter; ``` ## Dynamically load routers {#lazy-load} You can use the `lazy` function to dynamically load your routers. This can be useful to reduce cold starts of your application. There's no difference in how you use the router after it's been lazy loaded vs. how you use a normal router. ```twoslash include lazytrpc import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const router = t.router; export const publicProcedure = t.procedure; ``` ```ts twoslash title='routers/greeting.ts' // @target: esnext // @filename: trpc.ts // @include: lazytrpc // @filename: routers/greeting.ts // ---cut--- import { router, publicProcedure } from '../trpc'; export const greetingRouter = router({ hello: publicProcedure.query(() => 'world'), }); ``` ```ts twoslash title='routers/user.ts' // @target: esnext // @filename: trpc.ts // @include: lazytrpc // @filename: routers/user.ts // ---cut--- import { router, publicProcedure } from '../trpc'; export const userRouter = router({ list: publicProcedure.query(() => ['John', 'Jane', 'Jim']), }); ``` ```ts twoslash title='routers/_app.ts' // @target: esnext // @filename: trpc.ts // @include: lazytrpc // @filename: routers/greeting.ts import { router, publicProcedure } from '../trpc'; export const greetingRouter = router({ hello: publicProcedure.query(() => 'world') }); // @filename: routers/user.ts import { router, publicProcedure } from '../trpc'; export const userRouter = router({ list: publicProcedure.query(() => ['John', 'Jane', 'Jim']) }); // @filename: routers/_app.ts // ---cut--- import { lazy } from '@trpc/server'; import { router } from '../trpc'; export const appRouter = router({ // Option 1: Short-hand when the module has exactly 1 router exported greeting: lazy(() => import('./greeting.js')), // Option 2: if exporting more than 1 router user: lazy(() => import('./user.js').then((m) => m.userRouter)), }); export type AppRouter = typeof appRouter; ``` -------------------------------------------------- --- id: metadata title: Metadata sidebar_label: Metadata slug: /server/metadata --- Procedure metadata allows you to add an optional procedure specific `meta` property which will be available in all [middleware](middlewares) function parameters. :::tip Use metadata together with [`trpc-openapi`](https://github.com/jlalmes/trpc-openapi) if you want to expose REST-compatible endpoints for your application. ::: ## Create router with typed metadata ```tsx twoslash import { initTRPC } from '@trpc/server'; type Context = { user: { name: string } | null }; interface Meta { authRequired: boolean; } export const t = initTRPC.context().meta().create(); export const appRouter = t.router({ // [...] }); ``` ## Example with per route authentication settings ```tsx twoslash title='server.ts' import { initTRPC, TRPCError } from '@trpc/server'; type Context = { user: { name: string } | null }; interface Meta { authRequired: boolean; } export const t = initTRPC.context().meta().create(); export const authedProcedure = t.procedure.use(async (opts) => { const { meta, next, ctx } = opts; // only check authorization if enabled if (meta?.authRequired && !ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next(); }); export const appRouter = t.router({ hello: authedProcedure.meta({ authRequired: false }).query(() => { return { greeting: 'hello world', }; }), protectedHello: authedProcedure.meta({ authRequired: true }).query(() => { return { greeting: 'hello-world', }; }), }); ``` ## Default meta, chaining, and shallow merging You can set default values for your meta type, and if you chain meta on top of a base procedure it will be shallow merged. ```tsx twoslash import { initTRPC } from '@trpc/server'; type Context = { user: { name: string } | null }; interface Meta { authRequired?: boolean; role?: 'user' | 'admin' } export const t = initTRPC .context() .meta() .create({ // Set a default value defaultMeta: { authRequired: false } }); const authMiddleware = t.middleware((opts) => opts.next()); const publicProcedure = t.procedure // ^ Default Meta: { authRequired: false } const authProcedure = publicProcedure .use(authMiddleware) .meta({ authRequired: true, role: 'user' }); // ^ Meta: { authRequired: true, role: 'user' } const adminProcedure = authProcedure .meta({ role: 'admin' }); // ^ Meta: { authRequired: true, role: 'admin' } ``` -------------------------------------------------- --- id: middlewares title: Middlewares sidebar_label: Middlewares slug: /server/middlewares --- You can add middleware(s) to a procedure with the `t.procedure.use()` method. The middleware(s) will wrap the invocation of the procedure and must call `opts.next()` and return its result. ## Authorization In the example below, any call to a `adminProcedure` will ensure that the user is an "admin" before executing. ```twoslash include admin import { TRPCError, initTRPC } from '@trpc/server'; interface Context { user?: { id: string; isAdmin: boolean; // [..] }; } const t = initTRPC.context().create(); export const publicProcedure = t.procedure; export const router = t.router; export const adminProcedure = publicProcedure.use(async (opts) => { const { ctx } = opts; if (!ctx.user?.isAdmin) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return opts.next({ ctx: { user: ctx.user, }, }); }); ``` ```ts twoslash // @include: admin ``` ```ts twoslash // @filename: trpc.ts // @include: admin // @filename: _app.ts // ---cut--- import { adminProcedure, publicProcedure, router } from './trpc'; const adminRouter = router({ secretPlace: adminProcedure.query(() => 'a key'), }); export const appRouter = router({ foo: publicProcedure.query(() => 'bar'), admin: adminRouter, }); ``` :::tip See [Error Handling](error-handling.md) to learn more about the `TRPCError` thrown in the above example. ::: ## Logging In the example below timings for queries are logged automatically. ```twoslash include trpclogger import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const publicProcedure = t.procedure; export const router = t.router; declare function logMock(...args: any[]): void; // ---cut--- export const loggedProcedure = publicProcedure.use(async (opts) => { const start = Date.now(); const result = await opts.next(); const durationMs = Date.now() - start; const meta = { path: opts.path, type: opts.type, durationMs }; result.ok ? console.log('OK request timing:', meta) : console.error('Non-OK request timing', meta); return result; }); ``` ```ts twoslash // @include: trpclogger ``` ```ts twoslash // @filename: trpc.ts // @include: trpclogger // @filename: _app.ts // ---cut--- import { loggedProcedure, router } from './trpc'; export const appRouter = router({ foo: loggedProcedure.query(() => 'bar'), abc: loggedProcedure.query(() => 'def'), }); ``` ## Context Extension "Context Extension" enables middlewares to dynamically add and override keys on a base procedure's context in a typesafe manner. Below we have an example of a middleware that changes properties of a context, the changes are then available to all chained consumers, such as other middlewares and procedures: ```ts twoslash // @target: esnext import { initTRPC, TRPCError } from '@trpc/server'; const t = initTRPC.context().create(); const publicProcedure = t.procedure; const router = t.router; // ---cut--- type Context = { // user is nullable user?: { id: string; }; }; const protectedProcedure = publicProcedure.use(async function isAuthed(opts) { const { ctx } = opts; // `ctx.user` is nullable if (!ctx.user) { // ^? throw new TRPCError({ code: 'UNAUTHORIZED' }); } return opts.next({ ctx: { // ✅ user value is known to be non-null now user: ctx.user, // ^? }, }); }); protectedProcedure.query((opts) => { const { ctx } = opts; return ctx.user; // ^? }); ``` ## Using `.concat()` to create reusable middlewares and plugins {#concat} :::tip - Creating middlewares using `t.middleware` has the limitation that the `Context` type is tied to the `Context` type of the tRPC instance. - Creating middlewares with `experimental_standaloneMiddleware()` has the limitation that you cannot define input parsers and similar tied to your module. ::: tRPC has an API called `.concat()` which allows you to independently define a partial procedure that can be used with any tRPC instance that matches the context and metadata of the plugin. This helper primarily targets creating plugins and libraries with tRPC. ```ts twoslash // @target: esnext // ------------------------------------------------ // 🧩🧩🧩 a library creating a reusable plugin 🧩🧩🧩 // @filename: myPlugin.ts import { initTRPC, TRPCError } from '@trpc/server'; export function createMyPlugin() { // When creating a plugin for tRPC, you use the same API as creating any other tRPC-app // this is the plugin's root `t`-object const t = initTRPC .context<{ // the procedure using the plugin will need to extend this context }>() .meta<{ // the base `initTRPC`-object of the application using this needs to extend this meta }>() .create(); return { // you can also add `.input()` if you want your plugin to do input validation pluginProc: t.procedure.use((opts) => { return opts.next({ ctx: { fromPlugin: 'hello from myPlugin' as const, }, }); }), }; } // ------------------------------------ // 🚀🚀🚀 the app using the plugin 🚀🚀🚀 // @filename: app.ts import { createMyPlugin } from './myPlugin'; import { initTRPC, TRPCError } from '@trpc/server'; // the app's root `t`-object const t = initTRPC .context<{ // ... }>() .create(); export const publicProcedure = t.procedure; export const router = t.router; // initialize the plugin (a real-world example would likely take options here) const plugin = createMyPlugin(); // create a base procedure using the plugin const procedureWithPlugin = publicProcedure .concat( plugin.pluginProc, ) .use(opts => { const { ctx } = opts; // ^? return opts.next() }) export const appRouter = router({ hello: procedureWithPlugin.query(opts => { return opts.ctx.fromPlugin; }) }) ``` ## Extending middlewares :::info We have prefixed this as `unstable_` as it's a new API, but you're safe to use it! [Read more](/docs/faq#unstable). ::: We have a powerful feature called `.pipe()` which allows you to extend middlewares in a typesafe manner. Below we have an example of a middleware that extends a base middleware(foo). Like the context extension example above, piping middlewares will change properties of the context, and procedures will receive the new context value. ```ts twoslash // @target: esnext import { initTRPC, TRPCError } from '@trpc/server'; const t = initTRPC.create(); const publicProcedure = t.procedure; const router = t.router; const middleware = t.middleware; // ---cut--- const fooMiddleware = t.middleware((opts) => { return opts.next({ ctx: { foo: 'foo' as const, }, }); }); const barMiddleware = fooMiddleware.unstable_pipe((opts) => { const { ctx } = opts; ctx.foo; // ^? return opts.next({ ctx: { bar: 'bar' as const, }, }); }); const barProcedure = publicProcedure.use(barMiddleware); barProcedure.query((opts) => { const { ctx } = opts; return ctx.bar; // ^? }); ``` Beware that the order in which you pipe your middlewares matter and that the context must overlap. An example of a forbidden pipe is shown below. Here, the `fooMiddleware` overrides the `ctx.a` while `barMiddleware` still expects the root context from the initialization in `initTRPC` - so piping `fooMiddleware` with `barMiddleware` would not work, while piping `barMiddleware` with `fooMiddleware` does work. ```ts twoslash import { initTRPC } from '@trpc/server'; const t = initTRPC .context<{ a: { b: 'a'; }; }>() .create(); const fooMiddleware = t.middleware((opts) => { const { ctx } = opts; ctx.a; // 👈 fooMiddleware expects `ctx.a` to be an object // ^? return opts.next({ ctx: { a: 'a' as const, // 👈 `ctx.a` is no longer an object }, }); }); const barMiddleware = t.middleware((opts) => { const { ctx } = opts; ctx.a; // 👈 barMiddleware expects `ctx.a` to be an object // ^? return opts.next({ ctx: { foo: 'foo' as const, }, }); }); // @errors: 2345 // ❌ `ctx.a` does not overlap from `fooMiddleware` to `barMiddleware` fooMiddleware.unstable_pipe(barMiddleware); // ✅ `ctx.a` overlaps from `barMiddleware` and `fooMiddleware` barMiddleware.unstable_pipe(fooMiddleware); ``` ## Experimental: standalone middlewares :::warning Deprecated This has been deprecated in favor of [`.concat()`](#concat). Use `.concat()` for new code. ::: tRPC has an experimental API called `experimental_standaloneMiddleware` which allows you to independently define a middleware that can be used with any tRPC instance. Creating middlewares using `t.middleware` has the limitation that the `Context` type is tied to the `Context` type of the tRPC instance. This means that you cannot use the same middleware with multiple tRPC instances that have different `Context` types. Using `experimental_standaloneMiddleware` you can create a middleware that explicitly defines its requirements, i.e. the Context, Input and Meta types: ```ts twoslash // @target: esnext import { experimental_standaloneMiddleware, initTRPC, TRPCError, } from '@trpc/server'; import * as z from 'zod'; const projectAccessMiddleware = experimental_standaloneMiddleware<{ ctx: { allowedProjects: string[] }; // defaults to 'object' if not defined input: { projectId: string }; // defaults to 'unknown' if not defined // 'meta', not defined here, defaults to 'object | undefined' }>().create((opts) => { if (!opts.ctx.allowedProjects.includes(opts.input.projectId)) { throw new TRPCError({ code: 'FORBIDDEN', message: 'Not allowed', }); } return opts.next(); }); const t1 = initTRPC .context<{ allowedProjects: string[]; }>() .create(); // ✅ `ctx.allowedProjects` satisfies "string[]" and `input.projectId` satisfies "string" const accessControlledProcedure = t1.procedure .input(z.object({ projectId: z.string() })) .use(projectAccessMiddleware); // @errors: 2345 // ❌ `ctx.allowedProjects` satisfies "string[]" but `input.projectId` does not satisfy "string" const accessControlledProcedure2 = t1.procedure .input(z.object({ projectId: z.number() })) .use(projectAccessMiddleware); // @errors: 2345 // ❌ `ctx.allowedProjects` does not satisfy "string[]" even though `input.projectId` satisfies "string" const t2 = initTRPC .context<{ allowedProjects: number[]; }>() .create(); const accessControlledProcedure3 = t2.procedure .input(z.object({ projectId: z.string() })) .use(projectAccessMiddleware); ``` Here is an example with multiple standalone middlewares: ```ts twoslash // @target: esnext import { experimental_standaloneMiddleware, initTRPC } from '@trpc/server'; import * as z from 'zod'; const t = initTRPC.create(); const schemaA = z.object({ valueA: z.string() }); const schemaB = z.object({ valueB: z.string() }); const valueAUppercaserMiddleware = experimental_standaloneMiddleware<{ input: z.infer; }>().create((opts) => { return opts.next({ ctx: { valueAUppercase: opts.input.valueA.toUpperCase() }, }); }); const valueBUppercaserMiddleware = experimental_standaloneMiddleware<{ input: z.infer; }>().create((opts) => { return opts.next({ ctx: { valueBUppercase: opts.input.valueB.toUpperCase() }, }); }); const combinedInputThatSatisfiesBothMiddlewares = z.object({ valueA: z.string(), valueB: z.string(), extraProp: z.string(), }); t.procedure .input(combinedInputThatSatisfiesBothMiddlewares) .use(valueAUppercaserMiddleware) .use(valueBUppercaserMiddleware) .query( ({ input: { valueA, valueB, extraProp }, ctx: { valueAUppercase, valueBUppercase }, }) => `valueA: ${valueA}, valueB: ${valueB}, extraProp: ${extraProp}, valueAUppercase: ${valueAUppercase}, valueBUppercase: ${valueBUppercase}`, ); ``` -------------------------------------------------- --- id: non-json-content-types title: Content Types sidebar_label: Content Types (JSON, FormData, File, Blob) slug: /server/non-json-content-types --- tRPC supports multiple content types as procedure inputs: JSON-serializable data, FormData, File, Blob, and other binary types. ## JSON (Default) By default, tRPC sends and receives JSON-serializable data. No extra configuration is needed — any input that can be serialized to JSON works out of the box with all links (`httpLink`, `httpBatchLink`, `httpBatchStreamLink`). ```ts twoslash // @target: esnext import { initTRPC } from '@trpc/server'; // ---cut--- import { z } from 'zod'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure.input(z.object({ name: z.string() })).query((opts) => { return { greeting: `Hello ${opts.input.name}` }; }), }); ``` ## Non-JSON Content Types In addition to JSON, tRPC can use FormData, File, and other binary types as procedure inputs. ### Client Setup :::info While tRPC natively supports several non-JSON serializable types, your client may need a little link configuration to support them depending on your setup. ::: `httpLink` supports non-JSON content types out of the box — if you're only using this link, your existing setup should work immediately. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpLink } from '@trpc/client'; import type { AppRouter } from './server'; createTRPCClient({ links: [ httpLink({ url: 'http://localhost:2022', }), ], }); ``` However, not all links support these content types. If you're using `httpBatchLink` or `httpBatchStreamLink`, you will need to include a `splitLink` and route requests based on the content type. ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink, httpLink, isNonJsonSerializable, splitLink, } from '@trpc/client'; import type { AppRouter } from './server'; const url = 'http://localhost:2022'; createTRPCClient({ links: [ splitLink({ condition: (op) => isNonJsonSerializable(op.input), true: httpLink({ url, }), false: httpBatchLink({ url, }), }), ], }); ``` If you are using `transformer` in your tRPC server, TypeScript requires that your tRPC client link defines `transformer` as well. Use this example as a base: ```ts twoslash // @filename: server.ts import { initTRPC } from '@trpc/server'; import superjson from 'superjson'; const t = initTRPC.create({ transformer: superjson }); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, httpBatchLink, httpLink, isNonJsonSerializable, splitLink, } from '@trpc/client'; import superjson from 'superjson'; import type { AppRouter } from './server'; const url = 'http://localhost:2022'; createTRPCClient({ links: [ splitLink({ condition: (op) => isNonJsonSerializable(op.input), true: httpLink({ url, transformer: { // request - convert data before sending to the tRPC server serialize: (data) => data, // response - convert the tRPC response before using it in client deserialize: (data) => superjson.deserialize(data), // or your other transformer }, }), false: httpBatchLink({ url, transformer: superjson, // or your other transformer }), }), ], }); ``` ### Server Setup :::info When a request is handled by tRPC, it takes care of parsing the request body based on the `Content-Type` header of the request. If you encounter errors like `Failed to parse body as XXX`, make sure that your server (e.g., Express, Next.js) isn't parsing the request body before tRPC handles it. ```ts twoslash // @filename: router.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: app.ts // ---cut--- // Example in express import express from 'express'; import * as trpcExpress from '@trpc/server/adapters/express'; import { appRouter } from './router'; // incorrect const app1 = express(); app1.use(express.json()); // this tries to parse body before tRPC. app1.post('/express/hello', (req, res) => { res.end(); }); // normal express route handler app1.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter })); // tRPC fails to parse body // correct const app2 = express(); app2.use('/express', express.json()); // do it only in "/express/*" path app2.post('/express/hello', (req, res) => { res.end(); }); app2.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter })); // tRPC can parse body ``` ::: #### `FormData` Input FormData is natively supported, and for more advanced usage you could also combine this with a library like [zod-form-data](https://www.npmjs.com/package/zod-form-data) to validate inputs in a type-safe way. ```ts twoslash // @target: esnext import { initTRPC } from '@trpc/server'; // ---cut--- import { z } from 'zod'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure.input(z.instanceof(FormData)).mutation((opts) => { const data = opts.input; // ^? return { greeting: `Hello ${data.get('name')}`, }; }), }); ``` For a more advanced code sample you can see our [example project here](https://github.com/juliusmarminge/trpc-interop/blob/66aa760141030ffc421cae1a3bda9b5f1ab340b6/src/server.ts#L28-L43) #### `File` and other Binary Type Inputs tRPC converts many octet content types to a `ReadableStream` which can be consumed in a procedure. Currently these are `Blob` `Uint8Array` and `File`. ```ts twoslash // @target: esnext import { initTRPC } from '@trpc/server'; // ---cut--- import { octetInputParser } from '@trpc/server/http'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ upload: publicProcedure.input(octetInputParser).mutation((opts) => { const data = opts.input; // ^? return { valid: true, }; }), }); ``` -------------------------------------------------- --- id: overview title: Backend Usage sidebar_label: Overview slug: /server/overview --- This section covers everything you need to set up and configure your tRPC backend. You'll learn how to define [routers](/docs/server/routers) and [procedures](/docs/server/procedures), set up [context](/docs/server/context), add [input validation](/docs/server/validators), and use [middlewares](/docs/server/middlewares) to extend your API. Once your API is defined, you can host it using one of the available [adapters](/docs/server/adapters) for platforms like Express, Fastify, Next.js, AWS Lambda, and more. -------------------------------------------------- --- id: procedures title: Define Procedures sidebar_label: Define Procedures slug: /server/procedures --- A procedure is a function which is exposed to the client, it can be one of: - a `Query` - used to fetch data, generally does not change any data - a `Mutation` - used to send data, often for create/update/delete purposes - a `Subscription` - you might not need this, and we have [dedicated documentation](../server/subscriptions.md) Procedures in tRPC are very flexible primitives to create backend functions. They use an immutable builder pattern, which means you can [create reusable base procedures](#reusable-base-procedures) that share functionality among multiple procedures. ## Writing procedures The `t` object you create during tRPC setup returns an initial `t.procedure` which all other procedures are built on: ```ts twoslash import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.context<{ signGuestBook: () => Promise }>().create(); export const router = t.router; export const publicProcedure = t.procedure; const appRouter = router({ // Queries are the best place to fetch data hello: publicProcedure.query(() => { return { message: 'hello world', }; }), // Mutations are the best place to do things like updating a database goodbye: publicProcedure.mutation(async (opts) => { await opts.ctx.signGuestBook(); return { message: 'goodbye!', }; }), }); ``` ## Reusable "Base Procedures" {#reusable-base-procedures} As a general pattern we recommend you rename and export `t.procedure` as `publicProcedure`, which then makes room for you to create other named procedures for specific use cases and export those too. This pattern is called "base procedures" and is a key pattern for code and behaviour re-use in tRPC; every application is likely to need it. In the below code, we're using reusable base procedures to build common use-cases for our app - we're making a reusable base procedures for logged in users (`authedProcedure`) & another base procedure that takes an `organizationId` and validates that a user is part of that organization. > This is a simplified example; in practice you may want to use some combination of [Headers](/docs/client/headers), [Context](context), [Middleware](middlewares), and [Metadata](metadata), to [authenticate](https://en.wikipedia.org/wiki/Authentication) and [authorize](authorization) your users. ```ts twoslash // @target: esnext import { initTRPC, TRPCError } from '@trpc/server'; import { z } from 'zod'; type Organization = { id: string; name: string; }; type Membership = { role: 'ADMIN' | 'MEMBER'; Organization: Organization; }; type User = { id: string; memberships: Membership[]; }; type Context = { /** * User is nullable */ user: User | null; }; const t = initTRPC.context().create(); export const publicProcedure = t.procedure; // procedure that asserts that the user is logged in export const authedProcedure = t.procedure.use(async function isAuthed(opts) { const { ctx } = opts; // `ctx.user` is nullable if (!ctx.user) { // ^? throw new TRPCError({ code: 'UNAUTHORIZED' }); } return opts.next({ ctx: { // ✅ user value is known to be non-null now user: ctx.user, }, }); }); // procedure that asserts a user is a member of a specific organization export const organizationProcedure = authedProcedure .input(z.object({ organizationId: z.string() })) .use(function isMemberOfOrganization(opts) { const membership = opts.ctx.user.memberships.find( (m) => m.Organization.id === opts.input.organizationId, ); if (!membership) { throw new TRPCError({ code: 'FORBIDDEN', }); } return opts.next({ ctx: { Organization: membership.Organization, }, }); }); export const appRouter = t.router({ whoami: authedProcedure.query(async (opts) => { // user is non-nullable here const { ctx } = opts; // ^? return ctx.user; }), addMember: organizationProcedure .input( z.object({ email: z.string().email(), }), ) .mutation((opts) => { // ctx contains the non-nullable user & the organization being queried const { ctx } = opts; // ^? // input includes the validated email of the user being invited & the validated organizationId const { input } = opts; // ^? return '...'; }), }); ``` ## Inferring the options type of a "Base Procedure" {#inferProcedureBuilderResolverOptions} In addition to being able to [infer the input and output types](/docs/client/vanilla/infer-types#inferring-input--output-types) of a procedure, you can also infer the options type of a specific procedure builder (or base procedure) using `inferProcedureBuilderResolverOptions`. This type helper is useful for declaring a type to a function's parameters. Like for example, separating the procedure's handler (main execution code) from its definition at the router, or for creating a helper function that works with multiple procedures. ```ts twoslash // @target: esnext // ---cut--- import { inferProcedureBuilderResolverOptions, initTRPC, TRPCError, } from '@trpc/server'; import { z } from 'zod'; type Organization = { id: string; name: string; }; type Membership = { role: 'ADMIN' | 'MEMBER'; Organization: Organization; }; type User = { id: string; memberships: Membership[]; }; type Context = { /** * User is nullable */ user: User | null; }; const t = initTRPC.context().create(); export const publicProcedure = t.procedure; // procedure that asserts that the user is logged in export const authedProcedure = t.procedure.use(async function isAuthed(opts) { const { ctx } = opts; // `ctx.user` is nullable if (!ctx.user) { // ^? throw new TRPCError({ code: 'UNAUTHORIZED' }); } return opts.next({ ctx: { // ✅ user value is known to be non-null now user: ctx.user, }, }); }); // mock prisma let prisma = {} as any; // procedure that asserts a user is a member of a specific organization export const organizationProcedure = authedProcedure .input(z.object({ organizationId: z.string() })) .use(function isMemberOfOrganization(opts) { const membership = opts.ctx.user.memberships.find( (m) => m.Organization.id === opts.input.organizationId, ); if (!membership) { throw new TRPCError({ code: 'FORBIDDEN', }); } return opts.next({ ctx: { Organization: membership.Organization, }, }); }); // ---cut--- async function getMembersOfOrganization( opts: inferProcedureBuilderResolverOptions, ) { // input and ctx are now correctly typed! const { ctx, input } = opts; return await prisma.user.findMany({ where: { membership: { organizationId: ctx.Organization.id, }, }, }); } export const appRouter = t.router({ listMembers: organizationProcedure.query(async (opts) => { // use helper function! const members = await getMembersOfOrganization(opts); return members; }), }); ``` ## Subscriptions For information on subscriptions, see [our subscriptions guide](../server/subscriptions.md). -------------------------------------------------- --- id: routers title: Define Routers sidebar_label: Define Routers slug: /server/routers --- To begin building your tRPC-based API, you'll first need to define your router. Once you've mastered the fundamentals, you can [customize your routers](#advanced-usage) for more advanced use cases. ## Initialize tRPC You should initialize tRPC **exactly once** per application. Multiple instances of tRPC will cause issues. ```ts twoslash title='server/trpc.ts' // @filename: trpc.ts // ---cut--- import { initTRPC } from '@trpc/server'; // You can use any variable name you like. // We use t to keep things simple. const t = initTRPC.create(); export const router = t.router; export const publicProcedure = t.procedure; ``` You'll notice we are exporting certain methods of the `t` variable here rather than `t` itself. This is to establish a certain set of procedures that we will use idiomatically in our codebase. ## Defining a router Next, let's define a router with a procedure to use in our application. We have now created an API "endpoint". In order for these endpoints to be exposed to the frontend, your [Adapter](/docs/server/adapters) should be configured with your `appRouter` instance. ```ts twoslash title="server/_app.ts" // @filename: trpc.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const publicProcedure = t.procedure; export const router = t.router; // @filename: _app.ts import * as trpc from '@trpc/server'; // ---cut--- import { publicProcedure, router } from './trpc'; const appRouter = router({ greeting: publicProcedure.query(() => 'hello tRPC v11!'), }); // Export only the type of a router! // This prevents us from importing server code on the client. export type AppRouter = typeof appRouter; ``` ## Defining an inline sub-router {#inline-sub-router} When you define an inline sub-router, you can represent your router as a plain object. In the below example, `nested1` and `nested2` are equal: ```ts twoslash title="server/_app.ts" // @filename: trpc.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const middleware = t.middleware; export const publicProcedure = t.procedure; export const router = t.router; // @filename: _app.ts // ---cut--- import * as trpc from '@trpc/server'; import { publicProcedure, router } from './trpc'; const appRouter = router({ // Using the router() method nested1: router({ proc: publicProcedure.query(() => '...'), }), // Using an inline sub-router nested2: { proc: publicProcedure.query(() => '...'), }, }); ``` ## Advanced usage When initializing your router, tRPC allows you to: - Setup [request contexts](/docs/server/context) - Assign [metadata](/docs/server/metadata) to procedures - [Format](/docs/server/error-formatting) and [handle](/docs/server/error-handling) errors - [Transform data](/docs/server/data-transformers) as needed - Customize the [runtime configuration](#runtime-configuration) You can use method chaining to customize your `t`-object on initialization. For example: ```ts twoslash import { initTRPC } from '@trpc/server'; type Context = { userId: string }; type Meta = { description: string }; // ---cut--- const t = initTRPC.context().meta().create({ /* [...] */ }); ``` ### Runtime Configuration ```ts twoslash type DataTransformerOptions = any; type ErrorFormatter = any; // ---cut--- interface RootConfig { /** * Use a data transformer * @see https://trpc.io/docs/v11/data-transformers */ transformer: DataTransformerOptions; /** * Use custom error formatting * @see https://trpc.io/docs/v11/error-formatting */ errorFormatter: ErrorFormatter; /** * Allow `@trpc/server` to run in non-server environments * @warning **Use with caution**, this should likely mainly be used within testing. * @default false */ allowOutsideOfServer: boolean; /** * Is this a server environment? * @warning **Use with caution**, this should likely mainly be used within testing. * @default typeof window === 'undefined' || 'Deno' in window || process.env.NODE_ENV === 'test' */ isServer: boolean; /** * Is this development? * Will be used to decide if the API should return stack traces * @default process.env.NODE_ENV !== 'production' */ isDev: boolean; } ``` -------------------------------------------------- --- id: server-side-calls title: Server Side Calls sidebar_label: Server Side Calls slug: /server/server-side-calls --- You may need to call your procedure(s) directly from the same server they're hosted in, `createCallerFactory()` can be used to achieve this. This is useful for server-side calls and for integration testing of your tRPC procedures. :::info `createCaller` should not be used to call procedures from within other procedures. This creates overhead by (potentially) creating context again, executing all middlewares, and validating the input - all of which were already done by the current procedure. Instead, you should extract the shared logic into a separate function and call that from within the procedures, like so:
::: ## Create caller With the `t.createCallerFactory`-function you can create a server-side caller of any router. You first call `createCallerFactory` with an argument of the router you want to call, then this returns a function where you can pass in a `Context` for the following procedure calls. ### Basic example We create the router with a query to list posts and a mutation to add posts, and then we call each method. ```ts twoslash // @target: esnext import { initTRPC } from '@trpc/server'; import { z } from 'zod'; type Context = { foo: string; }; const t = initTRPC.context().create(); const publicProcedure = t.procedure; const { createCallerFactory, router } = t; interface Post { id: string; title: string; } const posts: Post[] = [ { id: '1', title: 'Hello world', }, ]; const appRouter = router({ post: router({ add: publicProcedure .input( z.object({ title: z.string().min(2), }), ) .mutation((opts) => { const post: Post = { ...opts.input, id: `${Math.random()}`, }; posts.push(post); return post; }), list: publicProcedure.query(() => posts), }), }); // 1. create a caller-function for your router const createCaller = createCallerFactory(appRouter); // 2. create a caller using your `Context` const caller = createCaller({ foo: 'bar', }); // 3. use the caller to add and list posts const addedPost = await caller.post.add({ title: 'How to make server-side call in tRPC', }); const postList = await caller.post.list(); // ^? ``` ### Example usage in an integration test > Taken from [https://github.com/trpc/examples-next-prisma-starter/blob/main/src/server/routers/post.test.ts](https://github.com/trpc/examples-next-prisma-starter/blob/main/src/server/routers/post.test.ts) ```ts twoslash // @target: esnext // @filename: context.ts export async function createContextInner(opts: {}) { return { user: undefined }; } // @filename: _app.ts import { initTRPC, type inferProcedureInput } from '@trpc/server'; import { z } from 'zod'; import { createContextInner } from './context'; type Context = Awaited>; const t = initTRPC.context().create(); const posts: { id: string; title: string; text: string }[] = []; export const appRouter = t.router({ post: t.router({ add: t.procedure .input(z.object({ title: z.string(), text: z.string() })) .mutation((opts) => { const post = { id: `${Math.random()}`, ...opts.input }; posts.push(post); return post; }), byId: t.procedure .input(z.object({ id: z.string() })) .query((opts) => posts.find((p) => p.id === opts.input.id)!), }), }); export type AppRouter = typeof appRouter; export const createCaller = t.createCallerFactory(appRouter); // @filename: test.ts import { type inferProcedureInput } from '@trpc/server'; import { createContextInner } from './context'; import { type AppRouter, createCaller } from './_app'; // ---cut--- async function testAddAndGetPost() { const ctx = await createContextInner({}); const caller = createCaller(ctx); const input: inferProcedureInput = { text: 'hello test', title: 'hello test', }; const post = await caller.post.add(input); const byId = await caller.post.byId({ id: post.id }); } ``` ## `router.createCaller()` With the `router.createCaller({})` function (first argument is `Context`) we retrieve an instance of `RouterCaller`. ### Input query example We create the router with an input query, and then we call the asynchronous `greeting` procedure to get the result. ```ts twoslash // @target: esnext import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const router = t.router({ // Create procedure at path 'greeting' greeting: t.procedure .input(z.object({ name: z.string() })) .query((opts) => `Hello ${opts.input.name}`), }); const caller = router.createCaller({}); const result = await caller.greeting({ name: 'tRPC' }); // ^? ``` ### Mutation example We create the router with a mutation, and then we call the asynchronous `post` procedure to get the result. ```ts twoslash // @target: esnext import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const posts = ['One', 'Two', 'Three']; const t = initTRPC.create(); const router = t.router({ post: t.router({ add: t.procedure.input(z.string()).mutation((opts) => { posts.push(opts.input); return posts; }), }), }); const caller = router.createCaller({}); const result = await caller.post.add('Four'); // ^? ``` ### Context with middleware example We create a middleware to check the context before executing the `secret` procedure. Below are two examples: the former fails because the context doesn't fit the middleware logic, and the latter works correctly.
:::info Middlewares are performed before any procedure(s) are called. :::
```ts twoslash // @target: esnext import { initTRPC, TRPCError } from '@trpc/server'; type Context = { user?: { id: string; }; }; const t = initTRPC.context().create(); const protectedProcedure = t.procedure.use((opts) => { const { ctx } = opts; if (!ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'You are not authorized', }); } return opts.next({ ctx: { // Infers that the `user` is non-nullable user: ctx.user, }, }); }); const router = t.router({ secret: protectedProcedure.query((opts) => opts.ctx.user), }); { // ❌ this will return an error because there isn't the right context param const caller = router.createCaller({}); const result = await caller.secret(); } { // ✅ this will work because user property is present inside context param const authorizedCaller = router.createCaller({ user: { id: 'KATT', }, }); const result = await authorizedCaller.secret(); // ^? } ``` ### Example for a Next.js API endpoint :::tip This example shows how to use the caller in a Next.js API endpoint. tRPC creates API endpoints for you already, so this file is only meant to show how to call a procedure from another, custom endpoint. ::: ```ts twoslash // @target: esnext // @filename: server/routers/_app.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); export const appRouter = t.router({ post: t.router({ byId: t.procedure .input(z.object({ id: z.string() })) .query((opts) => { return { id: opts.input.id, title: 'Example Post' }; }), }), }); // @filename: pages/api/post.ts import { TRPCError } from '@trpc/server'; import { getHTTPStatusCodeFromError } from '@trpc/server/http'; import { appRouter } from '../../server/routers/_app'; import type { NextApiRequest, NextApiResponse } from 'next'; // ---cut--- type ResponseData = { data?: { postTitle: string; }; error?: { message: string; }; }; export default async ( req: NextApiRequest, res: NextApiResponse, ) => { /** We want to simulate an error, so we pick a post ID that does not exist in the database. */ const postId = `this-id-does-not-exist-${Math.random()}`; const caller = appRouter.createCaller({}); try { // the server-side call const postResult = await caller.post.byId({ id: postId }); res.status(200).json({ data: { postTitle: postResult.title } }); } catch (cause) { // If this a tRPC error, we can extract additional information. if (cause instanceof TRPCError) { // We can get the specific HTTP status code coming from tRPC (e.g. 404 for `NOT_FOUND`). const httpStatusCode = getHTTPStatusCodeFromError(cause); res.status(httpStatusCode).json({ error: { message: cause.message } }); return; } // This is not a tRPC error, so we don't have specific information. res.status(500).json({ error: { message: `Error while accessing post with ID ${postId}` }, }); } }; ``` ### Error handling The `createCallerFactory` and the `createCaller` function can take an error handler through the `onError` option. This can be used to throw errors that are not wrapped in a TRPCError, or respond to errors in some other way. Any handler passed to createCallerFactory will be called before the handler passed to createCaller. The handler is called with the same arguments as an error formatter would be, except for the shape field: ```ts twoslash import { TRPCError } from '@trpc/server'; // ---cut--- interface OnErrorShape { ctx: unknown; error: TRPCError; path: string | undefined; input: unknown; type: 'query' | 'mutation' | 'subscription' | 'unknown'; } ``` ```ts twoslash // @target: esnext import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC .context<{ foo?: 'bar'; }>() .create(); const router = t.router({ greeting: t.procedure.input(z.object({ name: z.string() })).query((opts) => { if (opts.input.name === 'invalid') { throw new Error('Invalid name'); } return `Hello ${opts.input.name}`; }), }); const caller = router.createCaller( { /* context */ }, { onError: (opts) => { console.error('An error occurred:', opts.error); }, }, ); // The following will log "An error occurred: Error: Invalid name", and then throw the error await caller.greeting({ name: 'invalid' }); ``` -------------------------------------------------- --- id: subscriptions title: Subscriptions sidebar_label: Subscriptions slug: /server/subscriptions --- ## Introduction Subscriptions are a type of real-time event stream between the client and server. Use subscriptions when you need to push real-time updates to the client. With tRPC's subscriptions, the client establishes and maintains a persistent connection to the server plus automatically attempts to reconnect and recover gracefully if disconnected with the help of [`tracked()`](#tracked) events. ## WebSockets or Server-sent Events? You can either use WebSockets or [Server-sent Events](https://en.wikipedia.org/wiki/Server-sent_events) (SSE) to setup real-time subscriptions in tRPC. - For WebSockets, see [the WebSockets page](./websockets.md) - For SSE, see the [httpSubscriptionLink](../client/links/httpSubscriptionLink.md) If you are unsure which one to use, we recommend using SSE for subscriptions as it's easier to setup and doesn't require setting up a WebSocket server. ## Reference projects | Type | Example Type | Link | | ---------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | WebSockets | Bare-minimum Node.js WebSockets example | [/examples/standalone-server](https://github.com/trpc/trpc/tree/main/examples/standalone-server) | | SSE | Full-stack SSE implementation | [github.com/trpc/examples-next-sse-chat](https://github.com/trpc/examples-next-sse-chat) | | WebSockets | Full-stack WebSockets implementation | [github.com/trpc/examples-next-prisma-websockets-starter](https://github.com/trpc/examples-next-prisma-starter-websockets) | ## Basic example :::tip For a full example, see [our full-stack SSE example](https://github.com/trpc/examples-next-sse-chat). ::: ```ts twoslash title="server.ts" // @target: esnext // @types: node import EventEmitter, { on } from 'node:events'; import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); type Post = { id: string; title: string }; const ee = new EventEmitter(); export const appRouter = t.router({ onPostAdd: t.procedure.subscription(async function* (opts) { // listen for new events for await (const [data] of on(ee, 'add', { // Passing the AbortSignal from the request automatically cancels the event emitter when the request is aborted signal: opts.signal, })) { const post = data as Post; yield post; } }), }); ``` ## Automatic tracking of id using `tracked()` (recommended) {#tracked} If you `yield` an event using our `tracked()`-helper and include an `id`, the client will automatically reconnect when it gets disconnected and send the last known ID. You can send an initial `lastEventId` when initializing the subscription and it will be automatically updated as the browser receives data. - For SSE, this is part of the [`EventSource`-spec](https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header) and will be propagated through `lastEventId` in your `.input()`. - For WebSockets, our `wsLink` will automatically send the last known ID and update it as the browser receives data. :::tip If you're fetching data based on the `lastEventId`, and capturing all events is critical, make sure you set up the event listener before fetching events from your database as is done in [our full-stack SSE example](https://github.com/trpc/examples-next-sse-chat), this can prevent newly emitted events being ignored while yield'ing the original batch based on `lastEventId`. ::: ```ts twoslash // @types: node // @filename: index.ts // ---cut--- import EventEmitter, { on } from 'node:events'; import { initTRPC, tracked } from '@trpc/server'; import { z } from 'zod'; class IterableEventEmitter extends EventEmitter { toIterable(eventName: string, opts?: { signal?: AbortSignal }) { return on(this, eventName, opts); } } type Post = { id: string; title: string }; const t = initTRPC.create(); const publicProcedure = t.procedure; const router = t.router; const ee = new IterableEventEmitter(); export const subRouter = router({ onPostAdd: publicProcedure .input( z .object({ // lastEventId is the last event id that the client has received // On the first call, it will be whatever was passed in the initial setup // If the client reconnects, it will be the last event id that the client received lastEventId: z.string().nullish(), }) .optional(), ) .subscription(async function* (opts) { // We start by subscribing to the ee so that we don't miss any new events while fetching const iterable = ee.toIterable('add', { // Passing the AbortSignal from the request automatically cancels the event emitter when the request is aborted signal: opts.signal, }); if (opts.input?.lastEventId) { // [...] get the posts since the last event id and yield them // const items = await db.post.findMany({ ... }) // for (const item of items) { // yield tracked(item.id, item); // } } // listen for new events from the iterable we set up above for await (const [data] of iterable) { const post = data as Post; // tracking the post id ensures the client can reconnect at any time and get the latest events since this id yield tracked(post.id, post); } }), }); ``` ## Pull data in a loop This recipe is useful when you want to periodically check for new data from a source like a database and push it to the client. ```ts twoslash title="server.ts" // @filename: trpc.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const publicProcedure = t.procedure; export const router = t.router; // @filename: server.ts type Post = { id: string; title: string; createdAt: Date }; declare const db: { post: { findMany(opts: { where?: { createdAt?: { gt: Date } }; orderBy?: { createdAt: string } }): Promise; }; }; declare function sleep(ms: number): Promise; // ---cut--- import { tracked } from '@trpc/server'; import { z } from 'zod'; import { publicProcedure, router } from './trpc'; export const subRouter = router({ onPostAdd: publicProcedure .input( z.object({ // lastEventId is the last event id that the client has received // On the first call, it will be whatever was passed in the initial setup // If the client reconnects, it will be the last event id that the client received // The id is the createdAt of the post lastEventId: z.coerce.date().nullish(), }), ) .subscription(async function* (opts) { // `opts.signal` is an AbortSignal that will be aborted when the client disconnects. let lastEventId = opts.input?.lastEventId ?? null; // We use a `while` loop that checks `!opts.signal.aborted` while (!opts.signal!.aborted) { const posts = await db.post.findMany({ // If we have a `lastEventId`, we only fetch posts created after it. where: lastEventId ? { createdAt: { gt: lastEventId, }, } : undefined, orderBy: { createdAt: 'asc', }, }); for (const post of posts) { // `tracked` is a helper that sends an `id` with each event. // This allows the client to resume from the last received event upon reconnection. yield tracked(post.createdAt.toJSON(), post); lastEventId = post.createdAt; } // Wait for a bit before polling again to avoid hammering the database. await sleep(1_000); } }), }); ``` ## Stopping a subscription from the server {#stopping-from-server} If you need to stop a subscription from the server, simply `return` in the generator function. ```ts twoslash import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const publicProcedure = t.procedure; const router = t.router; // ---cut--- export const subRouter = router({ onPostAdd: publicProcedure .input( z.object({ lastEventId: z.coerce.number().min(0).optional(), }), ) .subscription(async function* (opts) { let index = opts.input.lastEventId ?? 0; while (!opts.signal!.aborted) { const idx = index++; if (idx > 100) { // With this, the subscription will stop and the client will disconnect return; } await new Promise((resolve) => setTimeout(resolve, 10)); } }), }); ``` On the client, you just `.unsubscribe()` the subscription. ## Cleanup of side effects If you need to clean up any side-effects of your subscription you can use the [`try...finally`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/return#using_return_with_try...finally) pattern, as `trpc` invokes the `.return()` of the Generator Instance when the subscription stops for any reason. ```ts twoslash // @types: node import EventEmitter, { on } from 'events'; import { initTRPC } from '@trpc/server'; type Post = { id: string; title: string }; const t = initTRPC.create(); const publicProcedure = t.procedure; const router = t.router; const ee = new EventEmitter(); export const subRouter = router({ onPostAdd: publicProcedure.subscription(async function* (opts) { let timeout: ReturnType | undefined; try { for await (const [data] of on(ee, 'add', { signal: opts.signal, })) { timeout = setTimeout(() => console.log('Pretend like this is useful')); const post = data as Post; yield post; } } finally { if (timeout) clearTimeout(timeout); } }), }); ``` ## Error handling Throwing an error in a generator function propagates to `trpc`'s `onError()` on the backend. If the error thrown is a 5xx error, the client will automatically attempt to reconnect based on the last event id that is [tracked using `tracked()`](#tracked). For other errors, the subscription will be cancelled and propagate to the `onError()` callback. ## Output validation {#output-validation} Since subscriptions are async iterators, you have to go through the iterator to validate the output. ### Example with Zod v4 ```ts twoslash title="zAsyncIterable.ts" // @target: esnext // @lib: esnext import type { TrackedEnvelope } from '@trpc/server'; import { isTrackedEnvelope, tracked } from '@trpc/server'; import { z } from 'zod'; function isAsyncIterable( value: unknown, ): value is AsyncIterable { return !!value && typeof value === 'object' && Symbol.asyncIterator in value; } const trackedEnvelopeSchema = z.custom>(isTrackedEnvelope); /** * A Zod schema helper designed specifically for validating async iterables. This schema ensures that: * 1. The value being validated is an async iterable. * 2. Each item yielded by the async iterable conforms to a specified type. * 3. The return value of the async iterable, if any, also conforms to a specified type. */ export function zAsyncIterable< TYieldIn, TYieldOut, TReturnIn = void, TReturnOut = void, Tracked extends boolean = false, >(opts: { /** * Validate the value yielded by the async generator */ yield: z.ZodType; /** * Validate the return value of the async generator * @remarks not applicable for subscriptions */ return?: z.ZodType; /** * Whether the yielded values are tracked * @remarks only applicable for subscriptions */ tracked?: Tracked; }) { return z .custom< AsyncIterable< Tracked extends true ? TrackedEnvelope : TYieldIn, TReturnIn > >((val) => isAsyncIterable(val)) .transform(async function* (iter) { const iterator = iter[Symbol.asyncIterator](); try { let next; while ((next = await iterator.next()) && !next.done) { if (opts.tracked) { const [id, data] = trackedEnvelopeSchema.parse(next.value); yield tracked(id, await opts.yield.parseAsync(data)); continue; } yield opts.yield.parseAsync(next.value); } if (opts.return) { return await opts.return.parseAsync(next.value); } return; } finally { await iterator.return?.(); } }) as z.ZodType< AsyncIterable< Tracked extends true ? TrackedEnvelope : TYieldIn, TReturnIn, unknown >, AsyncIterable< Tracked extends true ? TrackedEnvelope : TYieldOut, TReturnOut, unknown > >; } ``` Now you can use this helper to validate the output of your subscription procedures: ```ts twoslash title="_app.ts" // @target: esnext // @lib: esnext // @types: node // @filename: trpc.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const publicProcedure = t.procedure; export const router = t.router; // @filename: zAsyncIterable.ts import type { TrackedEnvelope } from '@trpc/server'; import { isTrackedEnvelope, tracked } from '@trpc/server'; import { z } from 'zod'; function isAsyncIterable(value: unknown): value is AsyncIterable { return !!value && typeof value === 'object' && Symbol.asyncIterator in value; } const trackedEnvelopeSchema = z.custom>(isTrackedEnvelope); export function zAsyncIterable(opts: { yield: z.ZodType; return?: z.ZodType; tracked?: Tracked; }) { return z.custom : TYieldIn, TReturnIn>>((val) => isAsyncIterable(val)).transform(async function* (iter) { const iterator = iter[Symbol.asyncIterator](); try { let next; while ((next = await iterator.next()) && !next.done) { if (opts.tracked) { const [id, data] = trackedEnvelopeSchema.parse(next.value); yield tracked(id, await opts.yield.parseAsync(data)); continue; } yield opts.yield.parseAsync(next.value); } if (opts.return) { return await opts.return.parseAsync(next.value); } return; } finally { await iterator.return?.(); } }) as z.ZodType : TYieldIn, TReturnIn, unknown>, AsyncIterable : TYieldOut, TReturnOut, unknown>>; } // @filename: _app.ts // ---cut--- import { tracked } from '@trpc/server'; import { z } from 'zod'; import { publicProcedure, router } from './trpc'; import { zAsyncIterable } from './zAsyncIterable'; export const appRouter = router({ mySubscription: publicProcedure .input( z.object({ lastEventId: z.coerce.number().min(0).optional(), }), ) .output( zAsyncIterable({ yield: z.object({ count: z.number(), }), tracked: true, }), ) .subscription(async function* (opts) { let index = opts.input.lastEventId ?? 0; while (true) { index++; yield tracked(String(index), { count: index, }); await new Promise((resolve) => setTimeout(resolve, 1000)); } }), }); ``` -------------------------------------------------- --- id: validators title: Input & Output Validators sidebar_label: Input & Output Validators slug: /server/validators --- tRPC procedures may define validation logic for their input and/or output, and validators are also used to infer the types of inputs and outputs (using the [Standard Schema](https://standardschema.dev) interface if available, or custom interfaces for supported validators if not). We have first class support for many popular validators, and you can [integrate validators](#contributing-your-own-validator-library) which we don't directly support. ## Input Validators By defining an input validator, tRPC can check that a procedure call is correct and return a validation error if not. To set up an input validator, use the `procedure.input()` method: ```ts twoslash // @target: esnext import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const publicProcedure = t.procedure; // ---cut--- export const appRouter = t.router({ hello: publicProcedure .input( z.object({ name: z.string(), }), ) .query((opts) => { const name = opts.input.name; // ^? return { greeting: `Hello ${opts.input.name}`, }; }), }); ``` ### Input Merging `.input()` can be stacked to build more complex types, which is particularly useful when you want to utilise some common input to a collection of procedures in a [middleware](middlewares). Input merging works by spreading object properties together. This means only **object types** can be chained — non-object types (like `z.string()`) cannot be merged. If two chained `.input()` calls define the same property, the later one takes precedence. ```ts twoslash // @target: esnext import { initTRPC, TRPCError } from '@trpc/server'; import { z } from 'zod'; export const t = initTRPC.create(); // ---cut--- const baseProcedure = t.procedure .input(z.object({ townName: z.string() })) .use((opts) => { const input = opts.input; // ^? console.log(`Handling request with user from: ${input.townName}`); return opts.next(); }); export const appRouter = t.router({ hello: baseProcedure .input( z.object({ name: z.string(), }), ) .query((opts) => { const input = opts.input; // ^? return { greeting: `Hello ${input.name}, my friend from ${input.townName}`, }; }), }); ``` ## Output Validators Validating outputs is not always as important as defining inputs, since tRPC gives you automatic type-safety by inferring the return type of your procedures. Some reasons to define an output validator include: - Checking that data returned from untrusted sources is correct - Ensure that you are not returning more data to the client than necessary :::info If output validation fails, the server will respond with an `INTERNAL_SERVER_ERROR`. ::: ```ts twoslash // @target: esnext import { initTRPC } from '@trpc/server'; // ---cut--- import { z } from 'zod'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure .output( z.object({ greeting: z.string(), }), ) .query((opts) => { return { greeting: 'hello world', //^? }; }), }); ``` ### Output validation of subscriptions Since subscriptions are async iterators, you can use the same validation techniques as above. Have a look at the [subscriptions guide](subscriptions.md#output-validation) for more information. ## The most basic validator: a function You can define a validator without any 3rd party dependencies, with a function. :::info We don't recommend making a custom validator unless you have a specific need, but it's important to understand that there's no magic here - it's _just typescript_! In most cases we recommend you use a [validation library](#library-integrations) ::: ```ts twoslash import { initTRPC } from '@trpc/server'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure .input((value): string => { if (typeof value === 'string') { return value; } throw new Error('Input is not a string'); }) .output((value): string => { if (typeof value === 'string') { return value; } throw new Error('Output is not a string'); }) .query((opts) => { const { input } = opts; // ^? return `hello ${input}`; }), }); export type AppRouter = typeof appRouter; ``` ## Library integrations tRPC works out of the box with a number of popular validation and parsing libraries, including any library conforming to [Standard Schema](https://standardschema.dev). The below are some examples of usage with validators which we officially maintain support for. ### With [Zod](https://github.com/colinhacks/zod) Zod is our default recommendation, it has a strong ecosystem which makes it a great choice to use in multiple parts of your codebase. If you have no opinion of your own and want a powerful library which won't limit future needs, Zod is a great choice. ```ts twoslash import { initTRPC } from '@trpc/server'; import { z } from 'zod'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure .input( z.object({ name: z.string(), }), ) .output( z.object({ greeting: z.string(), }), ) .query(({ input }) => { // ^? return { greeting: `hello ${input.name}`, }; }), }); export type AppRouter = typeof appRouter; ``` ### With [Yup](https://github.com/jquense/yup) ```ts twoslash import { initTRPC } from '@trpc/server'; import * as yup from 'yup'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure .input( yup.object({ name: yup.string().required(), }), ) .output( yup.object({ greeting: yup.string().required(), }), ) .query(({ input }) => { // ^? return { greeting: `hello ${input.name}`, }; }), }); export type AppRouter = typeof appRouter; ``` ### With [Superstruct](https://github.com/ianstormtaylor/superstruct) ```ts twoslash import { initTRPC } from '@trpc/server'; import { object, string } from 'superstruct'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure .input(object({ name: string() })) .output(object({ greeting: string() })) .query(({ input }) => { // ^? return { greeting: `hello ${input.name}`, }; }), }); export type AppRouter = typeof appRouter; ``` ### With [scale-ts](https://github.com/paritytech/scale-ts) ```ts twoslash import { initTRPC } from '@trpc/server'; import * as $ from 'scale-codec'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure .input($.object($.field('name', $.str))) .output($.object($.field('greeting', $.str))) .query(({ input }) => { // ^? return { greeting: `hello ${input.name}`, }; }), }); export type AppRouter = typeof appRouter; ``` ### With [Typia](https://typia.io/docs/utilization/trpc/) ```ts import { initTRPC } from '@trpc/server'; import typia from 'typia'; import { v4 } from 'uuid'; import { IBbsArticle } from '../structures/IBbsArticle'; const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ store: publicProcedure .input(typia.createAssert()) .output(typia.createAssert()) .query(({ input }) => { return { id: v4(), writer: input.writer, title: input.title, body: input.body, created_at: new Date().toString(), }; }), }); export type AppRouter = typeof appRouter; ``` ### With [ArkType](https://github.com/arktypeio/arktype#trpc) ```ts import { initTRPC } from '@trpc/server'; import { type } from 'arktype'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure.input(type({ name: 'string' })).query((opts) => { return { greeting: `hello ${opts.input.name}`, }; }), }); export type AppRouter = typeof appRouter; ``` ### With [effect](https://github.com/Effect-TS/effect/tree/main/packages/schema) ```ts twoslash import { initTRPC } from '@trpc/server'; import { Schema } from 'effect'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure .input(Schema.standardSchemaV1(Schema.Struct({ name: Schema.String }))) .output(Schema.standardSchemaV1(Schema.Struct({ greeting: Schema.String }))) .query(({ input }) => { // ^? return { greeting: `hello ${input.name}`, }; }), }); export type AppRouter = typeof appRouter; ``` ### With [Valibot](https://github.com/fabian-hiller/valibot) ```ts twoslash import { initTRPC } from '@trpc/server'; import * as v from 'valibot'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure .input(v.object({ name: v.string() })) .output(v.object({ greeting: v.string() })) .query(({ input }) => { // ^? return { greeting: `hello ${input.name}`, }; }), }); export type AppRouter = typeof appRouter; ``` ### With [@robolex/sure](https://github.com/robolex-app/public_ts) You're able to define your own Error types and error throwing function if necessary. As a convenience `@robolex/sure` provides [sure/src/err.ts](https://github.com/robolex-app/public_ts/blob/main/packages/sure/src/err.ts): ```ts // sure/src/err.ts export const err = (schema: any) => (input: any) => { const [good, result] = schema(input); if (good) return result; throw result; }; ``` ```ts import { err, object, string } from '@robolex/sure'; import { initTRPC } from '@trpc/server'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure .input( err( object({ name: string, }), ), ) .output( err( object({ greeting: string, }), ), ) .query(({ input }) => { // ^? return { greeting: `hello ${input.name}`, }; }), }); export type AppRouter = typeof appRouter; ``` ### With [TypeBox](https://github.com/sinclairzx81/typebox) ```ts import { Type } from '@sinclair/typebox'; import { initTRPC } from '@trpc/server'; import { wrap } from '@typeschema/typebox'; export const t = initTRPC.create(); const publicProcedure = t.procedure; export const appRouter = t.router({ hello: publicProcedure .input(wrap(Type.Object({ name: Type.String() }))) .output(wrap(Type.Object({ greeting: Type.String() }))) .query(({ input }) => { return { greeting: `hello ${input.name}`, }; }), }); export type AppRouter = typeof appRouter; ``` ## Contributing your own Validator Library If you work on a validator library which supports tRPC usage, please feel free to open a PR for this page with equivalent usage to the other examples here, and a link to your docs. Integration with tRPC in most cases is as simple as meeting one of several existing type interfaces. Conforming to [Standard Schema](https://standardschema.dev) is recommended, but in some cases we may accept a PR to add a new supported interface. Feel free to open an issue for discussion. You can check the existing supported interfaces and functions for parsing/validation [in code](https://github.com/trpc/trpc/blob/main/packages/server/src/unstable-core-do-not-import/parser.ts). -------------------------------------------------- --- id: websockets title: WebSockets sidebar_label: WebSockets slug: /server/websockets --- You can use WebSockets for all or some of the communication with your server, see [wsLink](../client/links/wsLink.md) for how to set it up on the client. :::tip The document here outlines the specific details of using WebSockets. For general usage of subscriptions, see [our subscriptions guide](../server/subscriptions.md). ::: ### Creating a WebSocket-server ```bash yarn add ws ``` ```ts twoslash title='server/wsServer.ts' // @filename: trpc.ts import type { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws'; export const createContext = (opts: CreateWSSContextFnOptions) => ({}); // @filename: routers/app.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); // @filename: wsServer.ts // @types: node // ---cut--- import { applyWSSHandler } from '@trpc/server/adapters/ws'; import { WebSocketServer } from 'ws'; import { appRouter } from './routers/app'; import { createContext } from './trpc'; const wss = new WebSocketServer({ port: 3001, }); const handler = applyWSSHandler({ wss, router: appRouter, createContext, // Enable heartbeat messages to keep connection open (disabled by default) keepAlive: { enabled: true, // server ping message interval in milliseconds pingMs: 30000, // connection is terminated if pong message is not received in this many milliseconds pongWaitMs: 5000, }, }); wss.on('connection', (ws) => { console.log(`++ Connection (${wss.clients.size})`); ws.once('close', () => { console.log(`-- Connection (${wss.clients.size})`); }); }); console.log('WebSocket Server listening on ws://localhost:3001'); process.on('SIGTERM', () => { console.log('SIGTERM'); handler.broadcastReconnectNotification(); wss.close(); }); ``` ### Setting `TRPCClient` to use WebSockets :::tip You can use [Links](../client/links/overview.md) to route queries and/or mutations to HTTP transport and subscriptions over WebSockets. ::: ```tsx twoslash title='client.ts' // @filename: server.ts import { initTRPC } from '@trpc/server'; const t = initTRPC.create(); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, createWSClient, wsLink } from '@trpc/client'; import type { AppRouter } from './server'; // create persistent WebSocket connection const wsClient = createWSClient({ url: `ws://localhost:3001`, }); // configure TRPCClient to use WebSockets transport const client = createTRPCClient({ links: [ wsLink({ client: wsClient, }), ], }); ``` ## Authentication / connection params {#connectionParams} :::tip If you're doing a web application, you can ignore this section as the cookies are sent as part of the request. ::: In order to authenticate with WebSockets, you can define `connectionParams` to `createWSClient`. This will be sent as the first message when the client establishes a WebSocket connection. ```ts twoslash title="server/context.ts" import type { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws'; export const createContext = async (opts: CreateWSSContextFnOptions) => { const token = opts.info.connectionParams?.token; // ^? // [... authenticate] return {}; }; export type Context = Awaited>; ``` ```ts twoslash title="client/trpc.ts" // @filename: server.ts import { initTRPC } from '@trpc/server'; import superjson from 'superjson'; const t = initTRPC.create({ transformer: superjson }); export const appRouter = t.router({}); export type AppRouter = typeof appRouter; // @filename: client.ts // ---cut--- import { createTRPCClient, createWSClient, wsLink } from '@trpc/client'; import type { AppRouter } from './server'; import superjson from 'superjson'; const wsClient = createWSClient({ url: `ws://localhost:3000`, connectionParams: async () => { return { token: 'supersecret', }; }, }); export const trpc = createTRPCClient({ links: [wsLink({ client: wsClient, transformer: superjson })], }); ``` ### Automatic tracking of id using `tracked()` (recommended) If you `yield` an event using our `tracked()`-helper and include an `id`, the client will automatically reconnect when it gets disconnected and send the last known ID when reconnecting as part of the `lastEventId`-input. You can send an initial `lastEventId` when initializing the subscription and it will be automatically updated as the browser receives data. :::info If you're fetching data based on the `lastEventId`, and capturing all events is critical, you may want to use `ReadableStream`'s or a similar pattern as an intermediary as is done in [our full-stack SSE example](https://github.com/trpc/examples-next-sse-chat) to prevent newly emitted events being ignored while yield'ing the original batch based on `lastEventId`. ::: ```ts twoslash // @types: node import EventEmitter, { on } from 'events'; import { initTRPC, tracked } from '@trpc/server'; import { z } from 'zod'; type Post = { id: string; title: string }; const t = initTRPC.create(); const publicProcedure = t.procedure; const router = t.router; const ee = new EventEmitter(); export const subRouter = router({ onPostAdd: publicProcedure .input( z .object({ // lastEventId is the last event id that the client has received // On the first call, it will be whatever was passed in the initial setup // If the client reconnects, it will be the last event id that the client received lastEventId: z.string().nullish(), }) .optional(), ) .subscription(async function* (opts) { if (opts.input?.lastEventId) { // [...] get the posts since the last event id and yield them } // listen for new events for await (const [data] of on(ee, 'add', { // Passing the AbortSignal from the request automatically cancels the event emitter when the subscription is aborted signal: opts.signal, })) { const post = data as Post; // tracking the post id ensures the client can reconnect at any time and get the latest events since this id yield tracked(post.id, post); } }), }); ``` ## WebSockets RPC Specification > You can read more details by drilling into the TypeScript definitions: > > - [/packages/server/src/unstable-core-do-not-import/rpc/envelopes.ts](https://github.com/trpc/trpc/tree/main/packages/server/src/unstable-core-do-not-import/rpc/envelopes.ts) > - [/packages/server/src/unstable-core-do-not-import/rpc/codes.ts](https://github.com/trpc/trpc/tree/main/packages/server/src/unstable-core-do-not-import/rpc/codes.ts). ### `query` / `mutation` #### Request ```ts twoslash interface RequestMessage { id: number | string; jsonrpc?: '2.0'; method: 'query' | 'mutation'; params: { path: string; input?: unknown; // <-- pass input of procedure, serialized by transformer }; } ``` #### Response _... below, or an error._ ```ts twoslash type TOutput = any; // ---cut--- interface ResponseMessage { id: number | string; jsonrpc?: '2.0'; result: { type: 'data'; // always 'data' for mutation / queries data: TOutput; // output from procedure }; } ``` ### `subscription` / `subscription.stop` #### Start a subscription ```ts twoslash interface SubscriptionRequest { id: number | string; jsonrpc?: '2.0'; method: 'subscription'; params: { path: string; input?: unknown; // <-- pass input of procedure, serialized by transformer }; } ``` #### To cancel a subscription, call `subscription.stop` ```ts twoslash interface SubscriptionStopRequest { id: number | string; // <-- id of your created subscription jsonrpc?: '2.0'; method: 'subscription.stop'; } ``` #### Subscription response shape _... below, or an error._ ```ts twoslash type TData = any; // ---cut--- interface SubscriptionResponse { id: number | string; jsonrpc?: '2.0'; result: | { type: 'data'; data: TData; // subscription emitted data } | { type: 'started'; // subscription started } | { type: 'stopped'; // subscription stopped }; } ``` #### Connection params If the connection is initialized with `?connectionParams=1`, the first message has to be connection params. ```ts twoslash interface ConnectionParamsMessage { data: Record | null; method: 'connectionParams'; } ``` ## Errors See [https://www.jsonrpc.org/specification#error_object](https://www.jsonrpc.org/specification#error_object) or [Error Formatting](../server/error-formatting.md). ## Notifications from Server to Client ### `{ id: null, type: 'reconnect' }` Tells clients to reconnect before shutting down the server. Invoked by `wssHandler.broadcastReconnectNotification()`. -------------------------------------------------- --- id: test title: Test --- ```twoslash include server // @target: esnext // @filename: server.ts import { initTRPC, TRPCError } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const posts = [ { id: '1', title: 'everlong' }, { id: '2', title: 'After Dark' }, ]; const appRouter = t.router({ post: t.router({ all: t.procedure .input( z.object({ cursor: z.string().optional(), }) ) .query(({ input }) => { return { posts, nextCursor: '123' as string | undefined, }; }), byId: t.procedure .input( z.object({ id: z.string(), }) ) .query(({ input }) => { const post = posts.find(p => p.id === input.id); if (!post) { throw new TRPCError({ code: 'NOT_FOUND', }) } return post; }), }), }); export type AppRouter = typeof appRouter; // @filename: utils/trpc.tsx // ---cut--- import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server'; export const trpc = createTRPCReact(); ``` ### Test ```tsx twoslash // @target: esnext // @include: server // @filename: pages/index.tsx import React from 'react'; // ---cut--- import { trpc } from '../utils/trpc'; function PostView() { const [{ pages }, allPostsQuery] = trpc.post.all.useSuspenseInfiniteQuery( {}, { getNextPageParam(lastPage) { return lastPage.nextCursor; }, initialCursor: '', }, ); return <>{/* ... */}; } ```