@trpc/next 11.14.0 → 11.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,6 +36,14 @@ pnpm add @trpc/next @trpc/react-query @tanstack/react-query
36
36
  bun add @trpc/next @trpc/react-query @tanstack/react-query
37
37
  ```
38
38
 
39
+ ## AI Agents
40
+
41
+ If you use an AI coding agent, install tRPC skills for better code generation:
42
+
43
+ ```bash
44
+ npx @tanstack/intent@latest install
45
+ ```
46
+
39
47
  ## Basic Example
40
48
 
41
49
  Setup tRPC in `utils/trpc.ts`.
package/bin/intent.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Auto-generated by @tanstack/intent setup
3
+ // Exposes the intent end-user CLI for consumers of this library.
4
+ // Commit this file, then add to your package.json:
5
+ // "bin": { "intent": "./bin/intent.js" }
6
+ try {
7
+ await import('@tanstack/intent/intent-library');
8
+ } catch (e) {
9
+ if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') {
10
+ console.error('@tanstack/intent is not installed.');
11
+ console.error('');
12
+ console.error('Install it as a dev dependency:');
13
+ console.error(' npm add -D @tanstack/intent');
14
+ console.error('');
15
+ console.error('Or run directly:');
16
+ console.error(' npx @tanstack/intent@latest list');
17
+ process.exit(1);
18
+ }
19
+ throw e;
20
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@trpc/next",
4
- "version": "11.14.0",
4
+ "version": "11.14.1",
5
5
  "description": "The tRPC Next.js library",
6
6
  "author": "KATT",
7
7
  "license": "MIT",
@@ -100,13 +100,16 @@
100
100
  "app-dir",
101
101
  "ssrPrepass",
102
102
  "!**/*.test.*",
103
- "!**/__tests__"
103
+ "!**/__tests__",
104
+ "skills",
105
+ "!skills/_artifacts",
106
+ "bin"
104
107
  ],
105
108
  "peerDependencies": {
106
109
  "@tanstack/react-query": "^5.59.15",
107
- "@trpc/client": "11.14.0",
108
- "@trpc/react-query": "11.14.0",
109
- "@trpc/server": "11.14.0",
110
+ "@trpc/client": "11.14.1",
111
+ "@trpc/react-query": "11.14.1",
112
+ "@trpc/server": "11.14.1",
110
113
  "next": "*",
111
114
  "react": ">=16.8.0",
112
115
  "react-dom": ">=16.8.0",
@@ -121,10 +124,11 @@
121
124
  }
122
125
  },
123
126
  "devDependencies": {
127
+ "@tanstack/intent": "^0.0.20",
124
128
  "@tanstack/react-query": "^5.80.3",
125
- "@trpc/client": "11.14.0",
126
- "@trpc/react-query": "11.14.0",
127
- "@trpc/server": "11.14.0",
129
+ "@trpc/client": "11.14.1",
130
+ "@trpc/react-query": "11.14.1",
131
+ "@trpc/server": "11.14.1",
128
132
  "@types/express": "^5.0.0",
129
133
  "@types/node": "^22.13.5",
130
134
  "@types/react": "^19.1.0",
@@ -145,5 +149,11 @@
145
149
  "funding": [
146
150
  "https://trpc.io/sponsor"
147
151
  ],
148
- "gitHead": "6e03f5c2f8d8ebaa237747d2db447737393402c6"
152
+ "keywords": [
153
+ "tanstack-intent"
154
+ ],
155
+ "bin": {
156
+ "intent": "./bin/intent.js"
157
+ },
158
+ "gitHead": "e896259af491fc4b1c9e8fc320817e2222bae869"
149
159
  }
@@ -0,0 +1,429 @@
1
+ ---
2
+ name: nextjs-app-router
3
+ description: >
4
+ Full end-to-end tRPC setup for Next.js App Router. Covers route handler
5
+ with fetchRequestHandler (GET + POST exports), TRPCProvider with
6
+ QueryClientProvider, createTRPCOptionsProxy for RSC prefetching,
7
+ HydrateClient/HydrationBoundary for hydration, useSuspenseQuery
8
+ for Suspense, and server-side callers.
9
+ type: framework
10
+ library: trpc
11
+ framework: react
12
+ library_version: '11.14.0'
13
+ requires:
14
+ - server-setup
15
+ - client-setup
16
+ - react-query-setup
17
+ - adapter-fetch
18
+ sources:
19
+ - www/docs/client/nextjs/overview.mdx
20
+ - www/docs/client/tanstack-react-query/server-components.mdx
21
+ - www/docs/server/adapters/nextjs.md
22
+ - examples/next-prisma-starter/
23
+ - examples/next-sse-chat/
24
+ ---
25
+
26
+ This skill builds on [server-setup], [client-setup], [react-query-setup], and [adapter-fetch]. Read them first for foundational concepts.
27
+
28
+ # tRPC -- Next.js App Router
29
+
30
+ ## File Structure
31
+
32
+ ```
33
+ .
34
+ ├── app
35
+ │ ├── api/trpc/[trpc]
36
+ │ │ └── route.ts # tRPC HTTP handler
37
+ │ ├── layout.tsx # mount TRPCReactProvider
38
+ │ ├── page.tsx # server component (prefetch)
39
+ │ └── client-greeting.tsx # client component (consume)
40
+ ├── trpc
41
+ │ ├── init.ts # initTRPC, createTRPCContext
42
+ │ ├── routers
43
+ │ │ └── _app.ts # main app router, AppRouter type
44
+ │ ├── query-client.ts # shared QueryClient factory
45
+ │ ├── client.tsx # client hooks & TRPCReactProvider
46
+ │ └── server.tsx # server-side proxy & helpers
47
+ └── ...
48
+ ```
49
+
50
+ ## Setup
51
+
52
+ ### 1. Install
53
+
54
+ ```sh
55
+ npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query zod server-only client-only
56
+ ```
57
+
58
+ ### 2. Server init and context
59
+
60
+ ```ts title="trpc/init.ts"
61
+ import { initTRPC } from '@trpc/server';
62
+
63
+ export const createTRPCContext = async (opts: { headers: Headers }) => {
64
+ return { userId: 'user_123' };
65
+ };
66
+
67
+ const t = initTRPC
68
+ .context<Awaited<ReturnType<typeof createTRPCContext>>>()
69
+ .create();
70
+
71
+ export const createTRPCRouter = t.router;
72
+ export const createCallerFactory = t.createCallerFactory;
73
+ export const baseProcedure = t.procedure;
74
+ ```
75
+
76
+ ### 3. Define the router
77
+
78
+ ```ts title="trpc/routers/_app.ts"
79
+ import { z } from 'zod';
80
+ import { baseProcedure, createTRPCRouter } from '../init';
81
+
82
+ export const appRouter = createTRPCRouter({
83
+ hello: baseProcedure
84
+ .input(z.object({ text: z.string() }))
85
+ .query(({ input }) => ({
86
+ greeting: `hello ${input.text}`,
87
+ })),
88
+ });
89
+
90
+ export type AppRouter = typeof appRouter;
91
+ ```
92
+
93
+ ### 4. Route handler (API endpoint)
94
+
95
+ ```ts title="app/api/trpc/[trpc]/route.ts"
96
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
97
+ import { createTRPCContext } from '../../../../trpc/init';
98
+ import { appRouter } from '../../../../trpc/routers/_app';
99
+
100
+ const handler = (req: Request) =>
101
+ fetchRequestHandler({
102
+ endpoint: '/api/trpc',
103
+ req,
104
+ router: appRouter,
105
+ createContext: () => createTRPCContext({ headers: req.headers }),
106
+ });
107
+
108
+ export { handler as GET, handler as POST };
109
+ ```
110
+
111
+ ### 5. QueryClient factory
112
+
113
+ ```ts title="trpc/query-client.ts"
114
+ import {
115
+ defaultShouldDehydrateQuery,
116
+ QueryClient,
117
+ } from '@tanstack/react-query';
118
+
119
+ export function makeQueryClient() {
120
+ return new QueryClient({
121
+ defaultOptions: {
122
+ queries: {
123
+ staleTime: 30 * 1000,
124
+ },
125
+ dehydrate: {
126
+ shouldDehydrateQuery: (query) =>
127
+ defaultShouldDehydrateQuery(query) ||
128
+ query.state.status === 'pending',
129
+ },
130
+ },
131
+ });
132
+ }
133
+ ```
134
+
135
+ If using a data transformer (e.g., superjson), add `dehydrate.serializeData` and `hydrate.deserializeData` here.
136
+
137
+ ### 6. Client provider (client component)
138
+
139
+ ```tsx title="trpc/client.tsx"
140
+ 'use client';
141
+
142
+ import type { QueryClient } from '@tanstack/react-query';
143
+ import { QueryClientProvider } from '@tanstack/react-query';
144
+ import { createTRPCClient, httpBatchLink } from '@trpc/client';
145
+ import { createTRPCContext } from '@trpc/tanstack-react-query';
146
+ import { useState } from 'react';
147
+ import { makeQueryClient } from './query-client';
148
+ import type { AppRouter } from './routers/_app';
149
+
150
+ export const { TRPCProvider, useTRPC, useTRPCClient } =
151
+ createTRPCContext<AppRouter>();
152
+
153
+ let browserQueryClient: QueryClient;
154
+ function getQueryClient() {
155
+ if (typeof window === 'undefined') {
156
+ return makeQueryClient();
157
+ }
158
+ if (!browserQueryClient) browserQueryClient = makeQueryClient();
159
+ return browserQueryClient;
160
+ }
161
+
162
+ function getUrl() {
163
+ const base = (() => {
164
+ if (typeof window !== 'undefined') return '';
165
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
166
+ return 'http://localhost:3000';
167
+ })();
168
+ return `${base}/api/trpc`;
169
+ }
170
+
171
+ export function TRPCReactProvider(props: { children: React.ReactNode }) {
172
+ const queryClient = getQueryClient();
173
+
174
+ const [trpcClient] = useState(() =>
175
+ createTRPCClient<AppRouter>({
176
+ links: [
177
+ httpBatchLink({
178
+ url: getUrl(),
179
+ }),
180
+ ],
181
+ }),
182
+ );
183
+
184
+ return (
185
+ <QueryClientProvider client={queryClient}>
186
+ <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
187
+ {props.children}
188
+ </TRPCProvider>
189
+ </QueryClientProvider>
190
+ );
191
+ }
192
+ ```
193
+
194
+ ### 7. Server-side proxy (server component)
195
+
196
+ ```tsx title="trpc/server.tsx"
197
+ import 'server-only';
198
+ import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
199
+ import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
200
+ import type { TRPCQueryOptions } from '@trpc/tanstack-react-query';
201
+ import { headers } from 'next/headers';
202
+ import { cache } from 'react';
203
+ import { createTRPCContext } from './init';
204
+ import { makeQueryClient } from './query-client';
205
+ import { appRouter } from './routers/_app';
206
+
207
+ export const getQueryClient = cache(makeQueryClient);
208
+
209
+ export const trpc = createTRPCOptionsProxy({
210
+ ctx: async () =>
211
+ createTRPCContext({
212
+ headers: await headers(),
213
+ }),
214
+ router: appRouter,
215
+ queryClient: getQueryClient,
216
+ });
217
+
218
+ export function HydrateClient(props: { children: React.ReactNode }) {
219
+ const queryClient = getQueryClient();
220
+ return (
221
+ <HydrationBoundary state={dehydrate(queryClient)}>
222
+ {props.children}
223
+ </HydrationBoundary>
224
+ );
225
+ }
226
+
227
+ export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
228
+ queryOptions: T,
229
+ ) {
230
+ const queryClient = getQueryClient();
231
+ if (queryOptions.queryKey[1]?.type === 'infinite') {
232
+ void queryClient.prefetchInfiniteQuery(queryOptions as any);
233
+ } else {
234
+ void queryClient.prefetchQuery(queryOptions);
235
+ }
236
+ }
237
+ ```
238
+
239
+ ### 8. Mount provider in layout
240
+
241
+ ```tsx title="app/layout.tsx"
242
+ import { TRPCReactProvider } from '../trpc/client';
243
+
244
+ export default function RootLayout({
245
+ children,
246
+ }: {
247
+ children: React.ReactNode;
248
+ }) {
249
+ return (
250
+ <html lang="en">
251
+ <body>
252
+ <TRPCReactProvider>{children}</TRPCReactProvider>
253
+ </body>
254
+ </html>
255
+ );
256
+ }
257
+ ```
258
+
259
+ ## Core Patterns
260
+
261
+ ### Prefetch in server component, consume in client component
262
+
263
+ ```tsx title="app/page.tsx"
264
+ import { HydrateClient, prefetch, trpc } from '../trpc/server';
265
+ import { ClientGreeting } from './client-greeting';
266
+
267
+ export default async function Home() {
268
+ prefetch(trpc.hello.queryOptions({ text: 'world' }));
269
+
270
+ return (
271
+ <HydrateClient>
272
+ <ClientGreeting />
273
+ </HydrateClient>
274
+ );
275
+ }
276
+ ```
277
+
278
+ ```tsx title="app/client-greeting.tsx"
279
+ 'use client';
280
+
281
+ import { useQuery } from '@tanstack/react-query';
282
+ import { useTRPC } from '../trpc/client';
283
+
284
+ export function ClientGreeting() {
285
+ const trpc = useTRPC();
286
+ const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));
287
+ if (!greeting.data) return <div>Loading...</div>;
288
+ return <div>{greeting.data.greeting}</div>;
289
+ }
290
+ ```
291
+
292
+ ### Suspense with prefetch
293
+
294
+ ```tsx title="app/page.tsx"
295
+ import { Suspense } from 'react';
296
+ import { ErrorBoundary } from 'react-error-boundary';
297
+ import { HydrateClient, prefetch, trpc } from '../trpc/server';
298
+ import { ClientGreeting } from './client-greeting';
299
+
300
+ export default async function Home() {
301
+ prefetch(trpc.hello.queryOptions({ text: 'world' }));
302
+
303
+ return (
304
+ <HydrateClient>
305
+ <ErrorBoundary fallback={<div>Something went wrong</div>}>
306
+ <Suspense fallback={<div>Loading...</div>}>
307
+ <ClientGreeting />
308
+ </Suspense>
309
+ </ErrorBoundary>
310
+ </HydrateClient>
311
+ );
312
+ }
313
+ ```
314
+
315
+ ```tsx title="app/client-greeting.tsx"
316
+ 'use client';
317
+
318
+ import { useSuspenseQuery } from '@tanstack/react-query';
319
+ import { useTRPC } from '../trpc/client';
320
+
321
+ export function ClientGreeting() {
322
+ const trpc = useTRPC();
323
+ const { data } = useSuspenseQuery(trpc.hello.queryOptions({ text: 'world' }));
324
+ return <div>{data.greeting}</div>;
325
+ }
326
+ ```
327
+
328
+ ### Direct server caller (data needed on server only)
329
+
330
+ ```tsx title="trpc/server.tsx"
331
+ // Add to existing server.tsx
332
+ export const caller = appRouter.createCaller(async () =>
333
+ createTRPCContext({ headers: await headers() }),
334
+ );
335
+ ```
336
+
337
+ ```tsx title="app/page.tsx"
338
+ import { caller } from '../trpc/server';
339
+
340
+ export default async function Home() {
341
+ const greeting = await caller.hello({ text: 'world' });
342
+ return <div>{greeting.greeting}</div>;
343
+ }
344
+ ```
345
+
346
+ Note: `caller` results are not stored in the query cache. They cannot hydrate to client components. Use `prefetchQuery` if client components also need the data.
347
+
348
+ ### fetchQuery for data on server AND client
349
+
350
+ ```tsx title="app/page.tsx"
351
+ import { getQueryClient, HydrateClient, trpc } from '../trpc/server';
352
+ import { ClientGreeting } from './client-greeting';
353
+
354
+ export default async function Home() {
355
+ const queryClient = getQueryClient();
356
+ const greeting = await queryClient.fetchQuery(
357
+ trpc.hello.queryOptions({ text: 'world' }),
358
+ );
359
+
360
+ // Use greeting on the server
361
+ console.log(greeting.greeting);
362
+
363
+ return (
364
+ <HydrateClient>
365
+ <ClientGreeting />
366
+ </HydrateClient>
367
+ );
368
+ }
369
+ ```
370
+
371
+ ## Common Mistakes
372
+
373
+ ### Not exporting both GET and POST from route handler
374
+
375
+ Next.js App Router route handlers must export named `GET` and `POST` functions. Missing either causes queries or mutations to return 405 Method Not Allowed.
376
+
377
+ ```ts
378
+ // WRONG
379
+ export default function handler(req: Request) { ... }
380
+
381
+ // CORRECT
382
+ const handler = (req: Request) =>
383
+ fetchRequestHandler({ req, router: appRouter, endpoint: '/api/trpc', createContext });
384
+ export { handler as GET, handler as POST };
385
+ ```
386
+
387
+ ### Creating a singleton QueryClient for SSR
388
+
389
+ In server components, each request needs its own `QueryClient` instance. A singleton leaks data between requests.
390
+
391
+ ```ts
392
+ // WRONG
393
+ const queryClient = new QueryClient(); // shared across requests!
394
+
395
+ // CORRECT
396
+ export const getQueryClient = cache(makeQueryClient);
397
+ ```
398
+
399
+ The `cache()` wrapper from React ensures the same `QueryClient` is reused within a single request but a new one is created for each new request.
400
+
401
+ ### Missing dehydrate/shouldDehydrateQuery config
402
+
403
+ RSC hydration requires `shouldDehydrateQuery` to include pending queries so that prefetched-but-not-yet-resolved promises can stream to the client. Without this, prefetched queries may not appear in the hydrated state.
404
+
405
+ ```ts
406
+ // WRONG
407
+ new QueryClient(); // default shouldDehydrateQuery skips pending
408
+
409
+ // CORRECT
410
+ new QueryClient({
411
+ defaultOptions: {
412
+ dehydrate: {
413
+ shouldDehydrateQuery: (query) =>
414
+ defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
415
+ },
416
+ },
417
+ });
418
+ ```
419
+
420
+ ### Suspense query failure crashes entire page during SSR
421
+
422
+ If a query fails during SSR with `useSuspenseQuery`, the entire page crashes. Error Boundaries only catch errors on the client side. For critical pages, either handle errors server-side before rendering, or use `useQuery` (non-suspense) which allows graceful degradation.
423
+
424
+ ## See Also
425
+
426
+ - [react-query-setup] -- TanStack React Query setup, queryOptions/mutationOptions factories
427
+ - [adapter-fetch] -- fetchRequestHandler for edge/serverless runtimes
428
+ - [server-setup] -- initTRPC, routers, procedures, context
429
+ - [nextjs-pages-router] -- if maintaining a Pages Router project alongside App Router
@@ -0,0 +1,371 @@
1
+ ---
2
+ name: nextjs-pages-router
3
+ description: >
4
+ Set up tRPC in Next.js Pages Router with createNextApiHandler,
5
+ createTRPCNext, withTRPC HOC, SSR via ssr option and ssrPrepass,
6
+ SSG via createServerSideHelpers with getStaticProps, and
7
+ server-side helpers for getServerSideProps prefetching.
8
+ type: framework
9
+ library: trpc
10
+ framework: react
11
+ library_version: '11.14.0'
12
+ requires:
13
+ - server-setup
14
+ - client-setup
15
+ sources:
16
+ - www/docs/client/nextjs/overview.mdx
17
+ - www/docs/server/adapters/nextjs.md
18
+ - examples/next-prisma-starter/
19
+ ---
20
+
21
+ This skill builds on [server-setup] and [client-setup]. Read them first for foundational concepts.
22
+
23
+ # tRPC -- Next.js Pages Router
24
+
25
+ ## File Structure
26
+
27
+ ```
28
+ .
29
+ ├── src
30
+ │ ├── pages
31
+ │ │ ├── _app.tsx # withTRPC() HOC
32
+ │ │ ├── api/trpc
33
+ │ │ │ └── [trpc].ts # tRPC API handler
34
+ │ │ └── index.tsx # page using tRPC hooks
35
+ │ ├── server
36
+ │ │ ├── routers
37
+ │ │ │ └── _app.ts # main app router
38
+ │ │ ├── context.ts # createContext
39
+ │ │ └── trpc.ts # initTRPC, procedure helpers
40
+ │ └── utils
41
+ │ └── trpc.ts # createTRPCNext, hooks
42
+ └── ...
43
+ ```
44
+
45
+ ## Setup
46
+
47
+ ### 1. Install
48
+
49
+ ```sh
50
+ npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod
51
+ ```
52
+
53
+ ### 2. Server init
54
+
55
+ ```ts title="server/trpc.ts"
56
+ import { initTRPC } from '@trpc/server';
57
+
58
+ const t = initTRPC.create();
59
+
60
+ export const router = t.router;
61
+ export const procedure = t.procedure;
62
+ ```
63
+
64
+ ### 3. Define the router
65
+
66
+ ```ts title="server/routers/_app.ts"
67
+ import { z } from 'zod';
68
+ import { procedure, router } from '../trpc';
69
+
70
+ export const appRouter = router({
71
+ hello: procedure.input(z.object({ text: z.string() })).query(({ input }) => ({
72
+ greeting: `hello ${input.text}`,
73
+ })),
74
+ });
75
+
76
+ export type AppRouter = typeof appRouter;
77
+ ```
78
+
79
+ ### 4. API handler
80
+
81
+ ```ts title="pages/api/trpc/[trpc].ts"
82
+ import { createNextApiHandler } from '@trpc/server/adapters/next';
83
+ import { appRouter } from '../../../server/routers/_app';
84
+
85
+ export default createNextApiHandler({
86
+ router: appRouter,
87
+ createContext: () => ({}),
88
+ });
89
+ ```
90
+
91
+ ### 5. Create tRPC hooks
92
+
93
+ ```ts title="utils/trpc.ts"
94
+ import { httpBatchLink } from '@trpc/client';
95
+ import { createTRPCNext } from '@trpc/next';
96
+ import type { AppRouter } from '../server/routers/_app';
97
+
98
+ function getBaseUrl() {
99
+ if (typeof window !== 'undefined') return '';
100
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
101
+ return `http://localhost:${process.env.PORT ?? 3000}`;
102
+ }
103
+
104
+ export const trpc = createTRPCNext<AppRouter>({
105
+ config() {
106
+ return {
107
+ links: [
108
+ httpBatchLink({
109
+ url: `${getBaseUrl()}/api/trpc`,
110
+ }),
111
+ ],
112
+ };
113
+ },
114
+ ssr: false,
115
+ });
116
+ ```
117
+
118
+ ### 6. Wrap app with withTRPC HOC
119
+
120
+ ```tsx title="pages/_app.tsx"
121
+ import type { AppType } from 'next/app';
122
+ import { trpc } from '../utils/trpc';
123
+
124
+ const MyApp: AppType = ({ Component, pageProps }) => {
125
+ return <Component {...pageProps} />;
126
+ };
127
+
128
+ export default trpc.withTRPC(MyApp);
129
+ ```
130
+
131
+ ### 7. Use hooks in pages
132
+
133
+ ```tsx title="pages/index.tsx"
134
+ import { trpc } from '../utils/trpc';
135
+
136
+ export default function IndexPage() {
137
+ const hello = trpc.hello.useQuery({ text: 'client' });
138
+ if (!hello.data) return <div>Loading...</div>;
139
+ return <p>{hello.data.greeting}</p>;
140
+ }
141
+ ```
142
+
143
+ ## Core Patterns
144
+
145
+ ### SSR with ssr: true
146
+
147
+ Enable SSR to prefetch all queries on the server automatically. Requires `ssrPrepass` and forwarding client headers.
148
+
149
+ ```ts title="utils/trpc.ts"
150
+ import { httpBatchLink } from '@trpc/client';
151
+ import { createTRPCNext } from '@trpc/next';
152
+ import { ssrPrepass } from '@trpc/next/ssrPrepass';
153
+ import type { AppRouter } from '../server/routers/_app';
154
+
155
+ function getBaseUrl() {
156
+ if (typeof window !== 'undefined') return '';
157
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
158
+ return `http://localhost:${process.env.PORT ?? 3000}`;
159
+ }
160
+
161
+ export const trpc = createTRPCNext<AppRouter>({
162
+ ssr: true,
163
+ ssrPrepass,
164
+ config({ ctx }) {
165
+ if (typeof window !== 'undefined') {
166
+ return {
167
+ links: [httpBatchLink({ url: '/api/trpc' })],
168
+ };
169
+ }
170
+ return {
171
+ links: [
172
+ httpBatchLink({
173
+ url: `${getBaseUrl()}/api/trpc`,
174
+ headers() {
175
+ if (!ctx?.req?.headers) return {};
176
+ return { cookie: ctx.req.headers.cookie };
177
+ },
178
+ }),
179
+ ],
180
+ };
181
+ },
182
+ });
183
+ ```
184
+
185
+ ### SSG with createServerSideHelpers and getStaticProps
186
+
187
+ ```tsx title="pages/posts/[id].tsx"
188
+ import { createServerSideHelpers } from '@trpc/react-query/server';
189
+ import type {
190
+ GetStaticPaths,
191
+ GetStaticPropsContext,
192
+ InferGetStaticPropsType,
193
+ } from 'next';
194
+ import superjson from 'superjson';
195
+ import { appRouter } from '../../server/routers/_app';
196
+ import { trpc } from '../../utils/trpc';
197
+
198
+ export async function getStaticProps(
199
+ context: GetStaticPropsContext<{ id: string }>,
200
+ ) {
201
+ const helpers = createServerSideHelpers({
202
+ router: appRouter,
203
+ ctx: {},
204
+ transformer: superjson,
205
+ });
206
+ const id = context.params?.id as string;
207
+
208
+ await helpers.post.byId.prefetch({ id });
209
+
210
+ return {
211
+ props: {
212
+ trpcState: helpers.dehydrate(),
213
+ id,
214
+ },
215
+ revalidate: 1,
216
+ };
217
+ }
218
+
219
+ export const getStaticPaths: GetStaticPaths = async () => {
220
+ return { paths: [], fallback: 'blocking' };
221
+ };
222
+
223
+ export default function PostPage(
224
+ props: InferGetStaticPropsType<typeof getStaticProps>,
225
+ ) {
226
+ const { id } = props;
227
+ const postQuery = trpc.post.byId.useQuery({ id });
228
+
229
+ if (postQuery.status !== 'success') return <>Loading...</>;
230
+ return <h1>{postQuery.data.title}</h1>;
231
+ }
232
+ ```
233
+
234
+ ### Server-side helpers with getServerSideProps
235
+
236
+ ```tsx title="pages/posts/[id].tsx"
237
+ import { createServerSideHelpers } from '@trpc/react-query/server';
238
+ import type {
239
+ GetServerSidePropsContext,
240
+ InferGetServerSidePropsType,
241
+ } from 'next';
242
+ import superjson from 'superjson';
243
+ import { appRouter } from '../../server/routers/_app';
244
+ import { trpc } from '../../utils/trpc';
245
+
246
+ export async function getServerSideProps(
247
+ context: GetServerSidePropsContext<{ id: string }>,
248
+ ) {
249
+ const helpers = createServerSideHelpers({
250
+ router: appRouter,
251
+ ctx: {},
252
+ transformer: superjson,
253
+ });
254
+ const id = context.params?.id as string;
255
+
256
+ await helpers.post.byId.prefetch({ id });
257
+
258
+ return {
259
+ props: {
260
+ trpcState: helpers.dehydrate(),
261
+ id,
262
+ },
263
+ };
264
+ }
265
+
266
+ export default function PostPage(
267
+ props: InferGetServerSidePropsType<typeof getServerSideProps>,
268
+ ) {
269
+ const { id } = props;
270
+ const postQuery = trpc.post.byId.useQuery({ id });
271
+
272
+ if (postQuery.status !== 'success') return <>Loading...</>;
273
+ return <h1>{postQuery.data.title}</h1>;
274
+ }
275
+ ```
276
+
277
+ ### SSR response caching
278
+
279
+ ```ts title="utils/trpc.ts"
280
+ import { httpBatchLink } from '@trpc/client';
281
+ import { createTRPCNext } from '@trpc/next';
282
+ import { ssrPrepass } from '@trpc/next/ssrPrepass';
283
+ import type { AppRouter } from '../server/routers/_app';
284
+
285
+ export const trpc = createTRPCNext<AppRouter>({
286
+ ssr: true,
287
+ ssrPrepass,
288
+ config() {
289
+ return {
290
+ links: [httpBatchLink({ url: '/api/trpc' })],
291
+ };
292
+ },
293
+ responseMeta(opts) {
294
+ const { clientErrors } = opts;
295
+ if (clientErrors.length) {
296
+ return { status: clientErrors[0].data?.httpStatus ?? 500 };
297
+ }
298
+ const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
299
+ return {
300
+ headers: new Headers([
301
+ [
302
+ 'cache-control',
303
+ `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
304
+ ],
305
+ ]),
306
+ };
307
+ },
308
+ });
309
+ ```
310
+
311
+ ### CORS on the API handler
312
+
313
+ ```ts title="pages/api/trpc/[trpc].ts"
314
+ import { createNextApiHandler } from '@trpc/server/adapters/next';
315
+ import type { NextApiRequest, NextApiResponse } from 'next';
316
+ import { appRouter } from '../../../server/routers/_app';
317
+
318
+ const nextApiHandler = createNextApiHandler({
319
+ router: appRouter,
320
+ createContext: () => ({}),
321
+ });
322
+
323
+ export default async function handler(
324
+ req: NextApiRequest,
325
+ res: NextApiResponse,
326
+ ) {
327
+ res.setHeader('Access-Control-Allow-Origin', '*');
328
+ res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
329
+ res.setHeader('Access-Control-Allow-Headers', '*');
330
+
331
+ if (req.method === 'OPTIONS') {
332
+ res.writeHead(200);
333
+ return res.end();
334
+ }
335
+
336
+ return nextApiHandler(req, res);
337
+ }
338
+ ```
339
+
340
+ ## Common Mistakes
341
+
342
+ ### Using ssr: true without understanding implications
343
+
344
+ Enabling `ssr: true` imports `react-dom` and runs `ssrPrepass` on every request, rendering the component tree repeatedly until no queries are fetching. This adds latency and server load. For better control, keep `ssr: false` (the default) and use `createServerSideHelpers` in `getServerSideProps` or `getStaticProps` to selectively prefetch only the queries you need.
345
+
346
+ ### SSR prepass renders multiple times
347
+
348
+ The SSR prepass loop re-renders the component tree repeatedly until all queries resolve. This is by design but causes performance issues with expensive renders. Keep SSR-rendered pages lightweight, or switch to selective prefetching with server-side helpers.
349
+
350
+ ### Mixing App Router and Pages Router patterns
351
+
352
+ App Router uses `fetchRequestHandler`, `createTRPCOptionsProxy`, and `@trpc/tanstack-react-query`. Pages Router uses `createNextApiHandler`, `createTRPCNext`, and `@trpc/next`/`@trpc/react-query`. Applying App Router patterns (like `HydrationBoundary` or `prefetchQuery`) in Pages Router, or vice versa, produces non-functional code.
353
+
354
+ ### Forgetting to return trpcState from getStaticProps/getServerSideProps
355
+
356
+ When using `createServerSideHelpers`, you must return `trpcState: helpers.dehydrate()` in props. Without this, the prefetched data is lost and queries re-fetch on the client.
357
+
358
+ ```ts
359
+ // WRONG
360
+ return { props: { id } }; // missing trpcState!
361
+
362
+ // CORRECT
363
+ return { props: { trpcState: helpers.dehydrate(), id } };
364
+ ```
365
+
366
+ ## See Also
367
+
368
+ - [server-setup] -- initTRPC, routers, procedures, context
369
+ - [client-setup] -- vanilla tRPC client, links configuration
370
+ - [nextjs-app-router] -- if migrating to or starting with App Router
371
+ - [react-query-classic-migration] -- migrating from @trpc/react-query to @trpc/tanstack-react-query