@trpc/server 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.
@@ -0,0 +1,205 @@
1
+ ---
2
+ name: caching
3
+ description: >
4
+ Set HTTP cache headers on tRPC query responses via responseMeta callback for
5
+ CDN and browser caching. Configure Cache-Control, s-maxage,
6
+ stale-while-revalidate. Handle caching with batching and authenticated requests.
7
+ Avoid caching mutations, errors, and authenticated responses.
8
+ type: core
9
+ library: trpc
10
+ library_version: '11.14.0'
11
+ requires:
12
+ - server-setup
13
+ sources:
14
+ - 'trpc/trpc:www/docs/server/caching.md'
15
+ ---
16
+
17
+ # tRPC -- Caching
18
+
19
+ ## Setup
20
+
21
+ ```ts
22
+ // server/trpc.ts
23
+ import { initTRPC } from '@trpc/server';
24
+ import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
25
+
26
+ export const createContext = async (opts: CreateHTTPContextOptions) => {
27
+ return {
28
+ req: opts.req,
29
+ res: opts.res,
30
+ user: null as { id: string } | null,
31
+ };
32
+ };
33
+
34
+ type Context = Awaited<ReturnType<typeof createContext>>;
35
+
36
+ export const t = initTRPC.context<Context>().create();
37
+ export const router = t.router;
38
+ export const publicProcedure = t.procedure;
39
+ ```
40
+
41
+ ```ts
42
+ // server/appRouter.ts
43
+ import { publicProcedure, router } from './trpc';
44
+
45
+ export const appRouter = router({
46
+ public: router({
47
+ slowQueryCached: publicProcedure.query(async () => {
48
+ await new Promise((resolve) => setTimeout(resolve, 5000));
49
+ return { lastUpdated: new Date().toJSON() };
50
+ }),
51
+ }),
52
+ });
53
+
54
+ export type AppRouter = typeof appRouter;
55
+ ```
56
+
57
+ ```ts
58
+ // server/index.ts
59
+ import { createHTTPServer } from '@trpc/server/adapters/standalone';
60
+ import { appRouter } from './appRouter';
61
+ import { createContext } from './trpc';
62
+
63
+ const server = createHTTPServer({
64
+ router: appRouter,
65
+ createContext,
66
+ responseMeta(opts) {
67
+ const { paths, errors, type } = opts;
68
+
69
+ const allPublic =
70
+ paths && paths.every((path) => path.startsWith('public.'));
71
+ const allOk = errors.length === 0;
72
+ const isQuery = type === 'query';
73
+
74
+ if (allPublic && allOk && isQuery) {
75
+ const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
76
+ return {
77
+ headers: new Headers([
78
+ [
79
+ 'cache-control',
80
+ `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
81
+ ],
82
+ ]),
83
+ };
84
+ }
85
+ return {};
86
+ },
87
+ });
88
+
89
+ server.listen(3000);
90
+ ```
91
+
92
+ ## Core Patterns
93
+
94
+ ### Path-based public route caching
95
+
96
+ ```ts
97
+ import { createHTTPServer } from '@trpc/server/adapters/standalone';
98
+ import { appRouter } from './appRouter';
99
+ import { createContext } from './trpc';
100
+
101
+ const server = createHTTPServer({
102
+ router: appRouter,
103
+ createContext,
104
+ responseMeta({ paths, errors, type }) {
105
+ const allPublic =
106
+ paths && paths.every((path) => path.startsWith('public.'));
107
+ const allOk = errors.length === 0;
108
+ const isQuery = type === 'query';
109
+
110
+ if (allPublic && allOk && isQuery) {
111
+ return {
112
+ headers: new Headers([
113
+ ['cache-control', 's-maxage=1, stale-while-revalidate=86400'],
114
+ ]),
115
+ };
116
+ }
117
+ return {};
118
+ },
119
+ });
120
+ ```
121
+
122
+ Name public routes with a `public` prefix (e.g., `public.slowQueryCached`) so `responseMeta` can identify them by path.
123
+
124
+ ### Skip caching for authenticated requests
125
+
126
+ ```ts
127
+ import { createHTTPServer } from '@trpc/server/adapters/standalone';
128
+ import { appRouter } from './appRouter';
129
+ import { createContext } from './trpc';
130
+
131
+ const server = createHTTPServer({
132
+ router: appRouter,
133
+ createContext,
134
+ responseMeta({ ctx, errors, type }) {
135
+ if (ctx?.user || errors.length > 0 || type !== 'query') {
136
+ return {};
137
+ }
138
+ return {
139
+ headers: new Headers([
140
+ ['cache-control', 's-maxage=1, stale-while-revalidate=86400'],
141
+ ]),
142
+ };
143
+ },
144
+ });
145
+ ```
146
+
147
+ ## Common Mistakes
148
+
149
+ ### [CRITICAL] Caching authenticated responses
150
+
151
+ Wrong:
152
+
153
+ ```ts
154
+ import { createHTTPServer } from '@trpc/server/adapters/standalone';
155
+ import { appRouter } from './appRouter';
156
+
157
+ const server = createHTTPServer({
158
+ router: appRouter,
159
+ responseMeta() {
160
+ return {
161
+ headers: new Headers([['cache-control', 's-maxage=60']]),
162
+ };
163
+ },
164
+ });
165
+ ```
166
+
167
+ Correct:
168
+
169
+ ```ts
170
+ import { createHTTPServer } from '@trpc/server/adapters/standalone';
171
+ import { appRouter } from './appRouter';
172
+ import { createContext } from './trpc';
173
+
174
+ const server = createHTTPServer({
175
+ router: appRouter,
176
+ createContext,
177
+ responseMeta({ ctx, errors, type }) {
178
+ if (ctx?.user || errors.length > 0 || type !== 'query') {
179
+ return {};
180
+ }
181
+ return {
182
+ headers: new Headers([
183
+ ['cache-control', 's-maxage=1, stale-while-revalidate=86400'],
184
+ ]),
185
+ };
186
+ },
187
+ });
188
+ ```
189
+
190
+ With batching enabled by default, a cached response containing personal data could be served to other users; always check for auth context, errors, and request type before setting cache headers.
191
+
192
+ Source: www/docs/server/caching.md
193
+
194
+ ### [HIGH] Next.js App Router overrides Cache-Control headers
195
+
196
+ There is no code fix for this -- Next.js App Router overrides `Cache-Control` headers set by tRPC via `responseMeta`. The documented caching approach using `responseMeta` does not work as expected in App Router. Use Next.js native caching mechanisms (`revalidate`, `unstable_cache`) instead when deploying on App Router.
197
+
198
+ Source: https://github.com/trpc/trpc/issues/5625
199
+
200
+ ## See Also
201
+
202
+ - `server-setup` -- initTRPC, createContext configuration
203
+ - `adapter-standalone` -- responseMeta option on createHTTPServer
204
+ - `adapter-fetch` -- responseMeta option on fetchRequestHandler
205
+ - `links` -- splitLink to separate public/private requests on the client
@@ -0,0 +1,253 @@
1
+ ---
2
+ name: error-handling
3
+ description: >
4
+ Throw typed errors with TRPCError and error codes (NOT_FOUND, UNAUTHORIZED,
5
+ BAD_REQUEST, INTERNAL_SERVER_ERROR), configure errorFormatter for client-side
6
+ Zod error display, handle errors globally with onError callback, map tRPC errors
7
+ to HTTP status codes with getHTTPStatusCodeFromError().
8
+ type: core
9
+ library: trpc
10
+ library_version: '11.14.0'
11
+ requires:
12
+ - server-setup
13
+ sources:
14
+ - 'trpc/trpc:www/docs/server/error-handling.md'
15
+ - 'trpc/trpc:www/docs/server/error-formatting.md'
16
+ - 'trpc/trpc:packages/server/src/unstable-core-do-not-import/error/TRPCError.ts'
17
+ ---
18
+
19
+ # tRPC -- Error Handling
20
+
21
+ ## Setup
22
+
23
+ ```ts
24
+ // server/trpc.ts
25
+ import { initTRPC } from '@trpc/server';
26
+ import { ZodError } from 'zod';
27
+
28
+ const t = initTRPC.create({
29
+ errorFormatter({ shape, error }) {
30
+ return {
31
+ ...shape,
32
+ data: {
33
+ ...shape.data,
34
+ zodError:
35
+ error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
36
+ ? error.cause.flatten()
37
+ : null,
38
+ },
39
+ };
40
+ },
41
+ });
42
+
43
+ export const router = t.router;
44
+ export const publicProcedure = t.procedure;
45
+ ```
46
+
47
+ ## Core Patterns
48
+
49
+ ### Throwing typed errors from procedures
50
+
51
+ ```ts
52
+ import { TRPCError } from '@trpc/server';
53
+ import { z } from 'zod';
54
+ import { publicProcedure, router } from './trpc';
55
+
56
+ export const appRouter = router({
57
+ userById: publicProcedure
58
+ .input(z.object({ id: z.string() }))
59
+ .query(({ input }) => {
60
+ const user = getUserFromDb(input.id);
61
+ if (!user) {
62
+ throw new TRPCError({
63
+ code: 'NOT_FOUND',
64
+ message: `User with id ${input.id} not found`,
65
+ });
66
+ }
67
+ return user;
68
+ }),
69
+ });
70
+
71
+ function getUserFromDb(id: string) {
72
+ if (id === '1') return { id: '1', name: 'Katt' };
73
+ return null;
74
+ }
75
+ ```
76
+
77
+ ### Wrapping original errors with cause
78
+
79
+ ```ts
80
+ import { TRPCError } from '@trpc/server';
81
+ import { publicProcedure, router } from './trpc';
82
+
83
+ export const appRouter = router({
84
+ riskyOperation: publicProcedure.mutation(async () => {
85
+ try {
86
+ return await externalService();
87
+ } catch (err) {
88
+ throw new TRPCError({
89
+ code: 'INTERNAL_SERVER_ERROR',
90
+ message: 'An unexpected error occurred, please try again later.',
91
+ cause: err,
92
+ });
93
+ }
94
+ }),
95
+ });
96
+
97
+ async function externalService() {
98
+ throw new Error('connection refused');
99
+ }
100
+ ```
101
+
102
+ Pass the original error as `cause` to retain the stack trace for debugging.
103
+
104
+ ### Global error handling with onError
105
+
106
+ ```ts
107
+ import { createHTTPServer } from '@trpc/server/adapters/standalone';
108
+ import { appRouter } from './appRouter';
109
+
110
+ const server = createHTTPServer({
111
+ router: appRouter,
112
+ onError(opts) {
113
+ const { error, type, path, input, ctx, req } = opts;
114
+ console.error('Error:', error);
115
+ if (error.code === 'INTERNAL_SERVER_ERROR') {
116
+ // send to bug reporting service
117
+ }
118
+ },
119
+ });
120
+
121
+ server.listen(3000);
122
+ ```
123
+
124
+ ### Extracting HTTP status from TRPCError
125
+
126
+ ```ts
127
+ import { TRPCError } from '@trpc/server';
128
+ import { getHTTPStatusCodeFromError } from '@trpc/server/http';
129
+
130
+ function handleError(error: unknown) {
131
+ if (error instanceof TRPCError) {
132
+ const httpCode = getHTTPStatusCodeFromError(error);
133
+ console.log(httpCode); // e.g., 400, 401, 404, 500
134
+ }
135
+ }
136
+ ```
137
+
138
+ ## Common Mistakes
139
+
140
+ ### [HIGH] Throwing plain Error instead of TRPCError
141
+
142
+ Wrong:
143
+
144
+ ```ts
145
+ import { publicProcedure } from './trpc';
146
+
147
+ const proc = publicProcedure.query(() => {
148
+ throw new Error('Not found');
149
+ // client receives 500 INTERNAL_SERVER_ERROR
150
+ });
151
+ ```
152
+
153
+ Correct:
154
+
155
+ ```ts
156
+ import { TRPCError } from '@trpc/server';
157
+ import { publicProcedure } from './trpc';
158
+
159
+ const proc = publicProcedure.query(() => {
160
+ throw new TRPCError({
161
+ code: 'NOT_FOUND',
162
+ message: 'User not found',
163
+ });
164
+ // client receives 404 NOT_FOUND
165
+ });
166
+ ```
167
+
168
+ Plain Error objects are caught and wrapped as INTERNAL_SERVER_ERROR (500); use TRPCError with a specific code for proper HTTP status mapping.
169
+
170
+ Source: www/docs/server/error-handling.md
171
+
172
+ ### [MEDIUM] Expecting stack traces in production
173
+
174
+ Wrong:
175
+
176
+ ```ts
177
+ import { initTRPC } from '@trpc/server';
178
+
179
+ // No explicit isDev setting
180
+ const t = initTRPC.create();
181
+ // Stack traces may or may not appear depending on NODE_ENV
182
+ ```
183
+
184
+ Correct:
185
+
186
+ ```ts
187
+ import { initTRPC } from '@trpc/server';
188
+
189
+ const t = initTRPC.create({
190
+ isDev: process.env.NODE_ENV === 'development',
191
+ });
192
+ ```
193
+
194
+ Stack traces are included only when `isDev` is true (default: `NODE_ENV !== "production"`); set `isDev` explicitly for deterministic behavior across runtimes.
195
+
196
+ Source: www/docs/server/error-handling.md
197
+
198
+ ### [HIGH] Not handling Zod errors in errorFormatter
199
+
200
+ Wrong:
201
+
202
+ ```ts
203
+ import { initTRPC } from '@trpc/server';
204
+
205
+ // No errorFormatter -- client gets generic "Input validation failed"
206
+ const t = initTRPC.create();
207
+ ```
208
+
209
+ Correct:
210
+
211
+ ```ts
212
+ import { initTRPC } from '@trpc/server';
213
+ import { ZodError } from 'zod';
214
+
215
+ const t = initTRPC.create({
216
+ errorFormatter({ shape, error }) {
217
+ return {
218
+ ...shape,
219
+ data: {
220
+ ...shape.data,
221
+ zodError:
222
+ error.code === 'BAD_REQUEST' && error.cause instanceof ZodError
223
+ ? error.cause.flatten()
224
+ : null,
225
+ },
226
+ };
227
+ },
228
+ });
229
+ ```
230
+
231
+ Without a custom errorFormatter, the client receives a generic message without field-level validation details from Zod.
232
+
233
+ Source: www/docs/server/error-formatting.md
234
+
235
+ ## Error Code Reference
236
+
237
+ | Code | HTTP | Use when |
238
+ | --------------------- | ---- | ------------------------------------ |
239
+ | BAD_REQUEST | 400 | Invalid input |
240
+ | UNAUTHORIZED | 401 | Missing or invalid auth credentials |
241
+ | FORBIDDEN | 403 | Authenticated but not authorized |
242
+ | NOT_FOUND | 404 | Resource does not exist |
243
+ | CONFLICT | 409 | Request conflicts with current state |
244
+ | UNPROCESSABLE_CONTENT | 422 | Valid syntax but semantic error |
245
+ | TOO_MANY_REQUESTS | 429 | Rate limit exceeded |
246
+ | INTERNAL_SERVER_ERROR | 500 | Unexpected server error |
247
+
248
+ ## See Also
249
+
250
+ - `server-setup` -- initTRPC configuration including isDev
251
+ - `validators` -- input validation that triggers BAD_REQUEST errors
252
+ - `middlewares` -- auth middleware throwing UNAUTHORIZED
253
+ - `server-side-calls` -- catching TRPCError in server-side callers
@@ -0,0 +1,242 @@
1
+ ---
2
+ name: middlewares
3
+ description: >
4
+ Create and compose tRPC middleware with t.procedure.use(), extend context via
5
+ opts.next({ ctx }), build reusable middleware with .concat() and .unstable_pipe(),
6
+ define base procedures like publicProcedure and authedProcedure. Access raw input
7
+ with getRawInput(). Logging, timing, OTEL tracing patterns.
8
+ type: core
9
+ library: trpc
10
+ library_version: '11.14.0'
11
+ requires:
12
+ - server-setup
13
+ sources:
14
+ - 'trpc/trpc:www/docs/server/middlewares.md'
15
+ - 'trpc/trpc:www/docs/server/authorization.md'
16
+ - 'trpc/trpc:packages/server/src/unstable-core-do-not-import/middleware.ts'
17
+ - 'trpc/trpc:packages/server/src/unstable-core-do-not-import/procedureBuilder.ts'
18
+ ---
19
+
20
+ # tRPC -- Middlewares
21
+
22
+ ## Setup
23
+
24
+ ```ts
25
+ // server/trpc.ts
26
+ import { initTRPC, TRPCError } from '@trpc/server';
27
+
28
+ type Context = {
29
+ user?: { id: string; isAdmin: boolean };
30
+ };
31
+
32
+ const t = initTRPC.context<Context>().create();
33
+
34
+ export const router = t.router;
35
+ export const publicProcedure = t.procedure;
36
+ export const middleware = t.middleware;
37
+ ```
38
+
39
+ ## Core Patterns
40
+
41
+ ### Auth middleware that narrows context type
42
+
43
+ ```ts
44
+ // server/trpc.ts
45
+ import { initTRPC, TRPCError } from '@trpc/server';
46
+
47
+ type Context = {
48
+ user?: { id: string; isAdmin: boolean };
49
+ };
50
+
51
+ const t = initTRPC.context<Context>().create();
52
+
53
+ export const publicProcedure = t.procedure;
54
+
55
+ export const authedProcedure = t.procedure.use(async (opts) => {
56
+ const { ctx } = opts;
57
+ if (!ctx.user) {
58
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
59
+ }
60
+ return opts.next({
61
+ ctx: {
62
+ user: ctx.user,
63
+ },
64
+ });
65
+ });
66
+
67
+ export const adminProcedure = t.procedure.use(async (opts) => {
68
+ const { ctx } = opts;
69
+ if (!ctx.user?.isAdmin) {
70
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
71
+ }
72
+ return opts.next({
73
+ ctx: {
74
+ user: ctx.user,
75
+ },
76
+ });
77
+ });
78
+ ```
79
+
80
+ After the middleware, `ctx.user` is non-nullable in downstream procedures.
81
+
82
+ ### Logging and timing middleware
83
+
84
+ ```ts
85
+ // server/trpc.ts
86
+ import { initTRPC } from '@trpc/server';
87
+
88
+ const t = initTRPC.create();
89
+
90
+ export const loggedProcedure = t.procedure.use(async (opts) => {
91
+ const start = Date.now();
92
+
93
+ const result = await opts.next();
94
+
95
+ const durationMs = Date.now() - start;
96
+ const meta = { path: opts.path, type: opts.type, durationMs };
97
+
98
+ result.ok
99
+ ? console.log('OK request timing:', meta)
100
+ : console.error('Non-OK request timing', meta);
101
+
102
+ return result;
103
+ });
104
+ ```
105
+
106
+ ### Reusable middleware with .concat()
107
+
108
+ ```ts
109
+ // myPlugin.ts
110
+ import { initTRPC } from '@trpc/server';
111
+
112
+ export function createMyPlugin() {
113
+ const t = initTRPC.context<{}>().meta<{}>().create();
114
+
115
+ return {
116
+ pluginProc: t.procedure.use((opts) => {
117
+ return opts.next({
118
+ ctx: {
119
+ fromPlugin: 'hello from myPlugin' as const,
120
+ },
121
+ });
122
+ }),
123
+ };
124
+ }
125
+ ```
126
+
127
+ ```ts
128
+ // server/trpc.ts
129
+ import { initTRPC } from '@trpc/server';
130
+ import { createMyPlugin } from './myPlugin';
131
+
132
+ const t = initTRPC.context<{}>().create();
133
+ const plugin = createMyPlugin();
134
+
135
+ export const publicProcedure = t.procedure;
136
+
137
+ export const procedureWithPlugin = publicProcedure.concat(plugin.pluginProc);
138
+ ```
139
+
140
+ `.concat()` merges a partial procedure (from any tRPC instance) into your procedure chain, as long as context and meta types overlap.
141
+
142
+ ### Extending middlewares with .unstable_pipe()
143
+
144
+ ```ts
145
+ import { initTRPC } from '@trpc/server';
146
+
147
+ const t = initTRPC.create();
148
+
149
+ const fooMiddleware = t.middleware((opts) => {
150
+ return opts.next({
151
+ ctx: { foo: 'foo' as const },
152
+ });
153
+ });
154
+
155
+ const barMiddleware = fooMiddleware.unstable_pipe((opts) => {
156
+ console.log(opts.ctx.foo);
157
+ return opts.next({
158
+ ctx: { bar: 'bar' as const },
159
+ });
160
+ });
161
+
162
+ const barProcedure = t.procedure.use(barMiddleware);
163
+ ```
164
+
165
+ Piped middlewares run in order and each receives the context from the previous middleware.
166
+
167
+ ## Common Mistakes
168
+
169
+ ### [CRITICAL] Forgetting to call and return opts.next()
170
+
171
+ Wrong:
172
+
173
+ ```ts
174
+ import { initTRPC } from '@trpc/server';
175
+
176
+ const t = initTRPC.create();
177
+
178
+ const logMiddleware = t.middleware(async (opts) => {
179
+ console.log('request started');
180
+ // forgot to call opts.next()
181
+ });
182
+ ```
183
+
184
+ Correct:
185
+
186
+ ```ts
187
+ import { initTRPC } from '@trpc/server';
188
+
189
+ const t = initTRPC.create();
190
+
191
+ const logMiddleware = t.middleware(async (opts) => {
192
+ console.log('request started');
193
+ const result = await opts.next();
194
+ console.log('request ended');
195
+ return result;
196
+ });
197
+ ```
198
+
199
+ Middleware must call `opts.next()` and return its result; forgetting this silently drops the request with an INTERNAL_SERVER_ERROR because no middleware marker is returned.
200
+
201
+ Source: packages/server/src/unstable-core-do-not-import/procedureBuilder.ts
202
+
203
+ ### [HIGH] Extending context with wrong type in opts.next()
204
+
205
+ Wrong:
206
+
207
+ ```ts
208
+ import { initTRPC } from '@trpc/server';
209
+
210
+ const t = initTRPC.create();
211
+
212
+ const middleware = t.middleware(async (opts) => {
213
+ return opts.next({ ctx: 'not-an-object' });
214
+ });
215
+ ```
216
+
217
+ Correct:
218
+
219
+ ```ts
220
+ import { initTRPC } from '@trpc/server';
221
+
222
+ const t = initTRPC.create();
223
+
224
+ async function getUser() {
225
+ return { id: '1', name: 'Katt' };
226
+ }
227
+
228
+ const middleware = t.middleware(async (opts) => {
229
+ return opts.next({ ctx: { user: await getUser() } });
230
+ });
231
+ ```
232
+
233
+ Context extension in `opts.next({ ctx })` must be an object; passing non-object values or overwriting required keys breaks downstream procedures.
234
+
235
+ Source: www/docs/server/middlewares.md
236
+
237
+ ## See Also
238
+
239
+ - `server-setup` -- initTRPC, routers, procedures, context
240
+ - `validators` -- input/output validation with Zod
241
+ - `error-handling` -- TRPCError codes used in auth middleware
242
+ - `auth` -- full auth patterns combining middleware + client headers