@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,265 @@
1
+ ---
2
+ name: non-json-content-types
3
+ description: >
4
+ Handle FormData, file uploads, Blob, Uint8Array, and ReadableStream inputs in
5
+ tRPC mutations. Use octetInputParser from @trpc/server/http for binary data.
6
+ Route non-JSON requests with splitLink and isNonJsonSerializable() from
7
+ @trpc/client. FormData and binary inputs only work with mutations (POST).
8
+ type: core
9
+ library: trpc
10
+ library_version: '11.14.0'
11
+ requires:
12
+ - server-setup
13
+ - links
14
+ sources:
15
+ - 'trpc/trpc:www/docs/server/non-json-content-types.md'
16
+ - 'trpc/trpc:examples/next-formdata/'
17
+ ---
18
+
19
+ # tRPC -- Non-JSON Content Types
20
+
21
+ ## Setup
22
+
23
+ Server:
24
+
25
+ ```ts
26
+ // server/trpc.ts
27
+ import { initTRPC } from '@trpc/server';
28
+
29
+ const t = initTRPC.create();
30
+
31
+ export const router = t.router;
32
+ export const publicProcedure = t.procedure;
33
+ ```
34
+
35
+ ```ts
36
+ // server/appRouter.ts
37
+ import { octetInputParser } from '@trpc/server/http';
38
+ import { z } from 'zod';
39
+ import { publicProcedure, router } from './trpc';
40
+
41
+ export const appRouter = router({
42
+ uploadForm: publicProcedure
43
+ .input(z.instanceof(FormData))
44
+ .mutation(({ input }) => {
45
+ const name = input.get('name');
46
+ return { greeting: `Hello ${name}` };
47
+ }),
48
+ uploadFile: publicProcedure.input(octetInputParser).mutation(({ input }) => {
49
+ // input is a ReadableStream
50
+ return { valid: true };
51
+ }),
52
+ });
53
+
54
+ export type AppRouter = typeof appRouter;
55
+ ```
56
+
57
+ Client:
58
+
59
+ ```ts
60
+ // client/index.ts
61
+ import {
62
+ createTRPCClient,
63
+ httpBatchLink,
64
+ httpLink,
65
+ isNonJsonSerializable,
66
+ splitLink,
67
+ } from '@trpc/client';
68
+ import type { AppRouter } from '../server/appRouter';
69
+
70
+ const url = 'http://localhost:3000';
71
+
72
+ const trpc = createTRPCClient<AppRouter>({
73
+ links: [
74
+ splitLink({
75
+ condition: (op) => isNonJsonSerializable(op.input),
76
+ true: httpLink({ url }),
77
+ false: httpBatchLink({ url }),
78
+ }),
79
+ ],
80
+ });
81
+ ```
82
+
83
+ ## Core Patterns
84
+
85
+ ### FormData mutation
86
+
87
+ ```ts
88
+ // server/appRouter.ts
89
+ import { z } from 'zod';
90
+ import { publicProcedure, router } from './trpc';
91
+
92
+ export const appRouter = router({
93
+ createPost: publicProcedure
94
+ .input(z.instanceof(FormData))
95
+ .mutation(({ input }) => {
96
+ const title = input.get('title') as string;
97
+ const body = input.get('body') as string;
98
+ return { id: '1', title, body };
99
+ }),
100
+ });
101
+ ```
102
+
103
+ ```ts
104
+ // client usage
105
+ const form = new FormData();
106
+ form.append('title', 'Hello');
107
+ form.append('body', 'World');
108
+
109
+ const result = await trpc.createPost.mutate(form);
110
+ ```
111
+
112
+ ### Binary file upload with octetInputParser
113
+
114
+ ```ts
115
+ // server/appRouter.ts
116
+ import { octetInputParser } from '@trpc/server/http';
117
+ import { publicProcedure, router } from './trpc';
118
+
119
+ export const appRouter = router({
120
+ upload: publicProcedure
121
+ .input(octetInputParser)
122
+ .mutation(async ({ input }) => {
123
+ const reader = input.getReader();
124
+ let totalBytes = 0;
125
+ while (true) {
126
+ const { done, value } = await reader.read();
127
+ if (done) break;
128
+ totalBytes += value.byteLength;
129
+ }
130
+ return { totalBytes };
131
+ }),
132
+ });
133
+ ```
134
+
135
+ ```ts
136
+ // client usage
137
+ const file = new File(['hello world'], 'test.txt', { type: 'text/plain' });
138
+ const result = await trpc.upload.mutate(file);
139
+ ```
140
+
141
+ `octetInputParser` converts Blob, Uint8Array, and File inputs to a ReadableStream on the server.
142
+
143
+ ### Client splitLink with superjson transformer
144
+
145
+ ```ts
146
+ import {
147
+ createTRPCClient,
148
+ httpBatchLink,
149
+ httpLink,
150
+ isNonJsonSerializable,
151
+ splitLink,
152
+ } from '@trpc/client';
153
+ import superjson from 'superjson';
154
+ import type { AppRouter } from '../server/appRouter';
155
+
156
+ const url = 'http://localhost:3000';
157
+
158
+ const trpc = createTRPCClient<AppRouter>({
159
+ links: [
160
+ splitLink({
161
+ condition: (op) => isNonJsonSerializable(op.input),
162
+ true: httpLink({
163
+ url,
164
+ transformer: {
165
+ serialize: (data) => data,
166
+ deserialize: (data) => superjson.deserialize(data),
167
+ },
168
+ }),
169
+ false: httpBatchLink({
170
+ url,
171
+ transformer: superjson,
172
+ }),
173
+ }),
174
+ ],
175
+ });
176
+ ```
177
+
178
+ When using a transformer, the non-JSON httpLink needs a custom transformer that skips serialization for the request (FormData/binary cannot be transformed) but deserializes the response.
179
+
180
+ ## Common Mistakes
181
+
182
+ ### [HIGH] Using httpBatchLink for FormData requests
183
+
184
+ Wrong:
185
+
186
+ ```ts
187
+ import { createTRPCClient, httpBatchLink } from '@trpc/client';
188
+ import type { AppRouter } from '../server/appRouter';
189
+
190
+ const trpc = createTRPCClient<AppRouter>({
191
+ links: [httpBatchLink({ url: 'http://localhost:3000' })],
192
+ });
193
+ ```
194
+
195
+ Correct:
196
+
197
+ ```ts
198
+ import {
199
+ createTRPCClient,
200
+ httpBatchLink,
201
+ httpLink,
202
+ isNonJsonSerializable,
203
+ splitLink,
204
+ } from '@trpc/client';
205
+ import type { AppRouter } from '../server/appRouter';
206
+
207
+ const url = 'http://localhost:3000';
208
+
209
+ const trpc = createTRPCClient<AppRouter>({
210
+ links: [
211
+ splitLink({
212
+ condition: (op) => isNonJsonSerializable(op.input),
213
+ true: httpLink({ url }),
214
+ false: httpBatchLink({ url }),
215
+ }),
216
+ ],
217
+ });
218
+ ```
219
+
220
+ FormData and binary inputs are not batchable; use `splitLink` with `isNonJsonSerializable()` to route them through `httpLink`.
221
+
222
+ Source: www/docs/server/non-json-content-types.md
223
+
224
+ ### [HIGH] Global body parser intercepting FormData before tRPC
225
+
226
+ Wrong:
227
+
228
+ ```ts
229
+ import * as trpcExpress from '@trpc/server/adapters/express';
230
+ import express from 'express';
231
+ import { appRouter } from './appRouter';
232
+
233
+ const app = express();
234
+ app.use(express.json());
235
+ app.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter }));
236
+ ```
237
+
238
+ Correct:
239
+
240
+ ```ts
241
+ import * as trpcExpress from '@trpc/server/adapters/express';
242
+ import express from 'express';
243
+ import { appRouter } from './appRouter';
244
+
245
+ const app = express();
246
+ app.use('/api', express.json());
247
+ app.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter }));
248
+ ```
249
+
250
+ A global `express.json()` middleware consumes the request body before tRPC can read it; scope body parsing to non-tRPC routes only.
251
+
252
+ Source: www/docs/server/non-json-content-types.md
253
+
254
+ ### [HIGH] FormData only works with mutations
255
+
256
+ FormData and binary inputs are only supported for mutations (POST requests). Using them with `.query()` throws an error because queries use HTTP GET which cannot carry a request body.
257
+
258
+ Source: www/docs/server/non-json-content-types.md
259
+
260
+ ## See Also
261
+
262
+ - `server-setup` -- initTRPC, routers, procedures
263
+ - `links` -- splitLink configuration for routing non-JSON requests
264
+ - `validators` -- z.instanceof(FormData) for FormData validation
265
+ - `adapter-express` -- Express-specific body parser considerations
@@ -0,0 +1,378 @@
1
+ ---
2
+ name: server-setup
3
+ description: >
4
+ Initialize tRPC with initTRPC.create(), define routers with t.router(),
5
+ create procedures with .query()/.mutation()/.subscription(), configure context
6
+ with createContext(), export AppRouter type, merge routers with t.mergeRouters(),
7
+ lazy-load routers with lazy().
8
+ type: core
9
+ library: trpc
10
+ library_version: '11.14.0'
11
+ requires: []
12
+ sources:
13
+ - 'trpc/trpc:www/docs/server/overview.md'
14
+ - 'trpc/trpc:www/docs/server/routers.md'
15
+ - 'trpc/trpc:www/docs/server/procedures.md'
16
+ - 'trpc/trpc:www/docs/server/context.md'
17
+ - 'trpc/trpc:www/docs/server/merging-routers.md'
18
+ - 'trpc/trpc:www/docs/main/quickstart.mdx'
19
+ - 'trpc/trpc:packages/server/src/unstable-core-do-not-import/initTRPC.ts'
20
+ - 'trpc/trpc:packages/server/src/unstable-core-do-not-import/router.ts'
21
+ ---
22
+
23
+ # tRPC -- Server Setup
24
+
25
+ ## Setup
26
+
27
+ ```ts
28
+ // server/trpc.ts
29
+ import { initTRPC } from '@trpc/server';
30
+
31
+ const t = initTRPC.create();
32
+
33
+ export const router = t.router;
34
+ export const publicProcedure = t.procedure;
35
+ ```
36
+
37
+ ```ts
38
+ // server/appRouter.ts
39
+ import { z } from 'zod';
40
+ import { publicProcedure, router } from './trpc';
41
+
42
+ type User = { id: string; name: string };
43
+
44
+ export const appRouter = router({
45
+ userList: publicProcedure.query(async (): Promise<User[]> => {
46
+ return [{ id: '1', name: 'Katt' }];
47
+ }),
48
+ userById: publicProcedure
49
+ .input(z.string())
50
+ .query(async ({ input }): Promise<User> => {
51
+ return { id: input, name: 'Katt' };
52
+ }),
53
+ userCreate: publicProcedure
54
+ .input(z.object({ name: z.string() }))
55
+ .mutation(async ({ input }): Promise<User> => {
56
+ return { id: '1', ...input };
57
+ }),
58
+ });
59
+
60
+ export type AppRouter = typeof appRouter;
61
+ ```
62
+
63
+ ```ts
64
+ // server/index.ts
65
+ import { createHTTPServer } from '@trpc/server/adapters/standalone';
66
+ import { appRouter } from './appRouter';
67
+
68
+ const server = createHTTPServer({ router: appRouter });
69
+ server.listen(3000);
70
+ ```
71
+
72
+ ## Core Patterns
73
+
74
+ ### Context with typed session
75
+
76
+ ```ts
77
+ // server/context.ts
78
+ import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
79
+
80
+ export async function createContext(opts: CreateHTTPContextOptions) {
81
+ const token = opts.req.headers['authorization'];
82
+ return { token };
83
+ }
84
+
85
+ export type Context = Awaited<ReturnType<typeof createContext>>;
86
+ ```
87
+
88
+ ```ts
89
+ // server/trpc.ts
90
+ import { initTRPC } from '@trpc/server';
91
+ import type { Context } from './context';
92
+
93
+ const t = initTRPC.context<Context>().create();
94
+
95
+ export const router = t.router;
96
+ export const publicProcedure = t.procedure;
97
+ ```
98
+
99
+ ```ts
100
+ // server/index.ts
101
+ import { createHTTPServer } from '@trpc/server/adapters/standalone';
102
+ import { appRouter } from './appRouter';
103
+ import { createContext } from './context';
104
+
105
+ const server = createHTTPServer({
106
+ router: appRouter,
107
+ createContext,
108
+ });
109
+ server.listen(3000);
110
+ ```
111
+
112
+ ### Inner/outer context split for testability
113
+
114
+ ```ts
115
+ // server/context.ts
116
+ import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
117
+ import { db } from './db';
118
+
119
+ interface CreateInnerContextOptions {
120
+ session: { user: { email: string } } | null;
121
+ }
122
+
123
+ export async function createContextInner(opts?: CreateInnerContextOptions) {
124
+ return {
125
+ db,
126
+ session: opts?.session ?? null,
127
+ };
128
+ }
129
+
130
+ export async function createContext(opts: CreateHTTPContextOptions) {
131
+ const session = getSessionFromCookie(opts.req);
132
+ const contextInner = await createContextInner({ session });
133
+ return {
134
+ ...contextInner,
135
+ req: opts.req,
136
+ res: opts.res,
137
+ };
138
+ }
139
+
140
+ export type Context = Awaited<ReturnType<typeof createContextInner>>;
141
+ ```
142
+
143
+ Infer `Context` from `createContextInner` so server-side callers and tests never need HTTP request objects.
144
+
145
+ ### Merging child routers
146
+
147
+ ```ts
148
+ // server/routers/user.ts
149
+ import { publicProcedure, router } from '../trpc';
150
+
151
+ export const userRouter = router({
152
+ list: publicProcedure.query(() => []),
153
+ });
154
+ ```
155
+
156
+ ```ts
157
+ // server/routers/post.ts
158
+ import { z } from 'zod';
159
+ import { publicProcedure, router } from '../trpc';
160
+
161
+ export const postRouter = router({
162
+ create: publicProcedure
163
+ .input(z.object({ title: z.string() }))
164
+ .mutation(({ input }) => ({ id: '1', ...input })),
165
+ list: publicProcedure.query(() => []),
166
+ });
167
+ ```
168
+
169
+ ```ts
170
+ // server/routers/_app.ts
171
+ import { router } from '../trpc';
172
+ import { postRouter } from './post';
173
+ import { userRouter } from './user';
174
+
175
+ export const appRouter = router({
176
+ user: userRouter,
177
+ post: postRouter,
178
+ });
179
+
180
+ export type AppRouter = typeof appRouter;
181
+ ```
182
+
183
+ ### Lazy-loaded routers for serverless cold starts
184
+
185
+ ```ts
186
+ // server/routers/_app.ts
187
+ import { lazy } from '@trpc/server';
188
+ import { router } from '../trpc';
189
+
190
+ export const appRouter = router({
191
+ // Short-hand when the module has exactly one router exported
192
+ greeting: lazy(() => import('./greeting.js')),
193
+ // Use .then() to pick a named export when the module exports multiple routers
194
+ user: lazy(() => import('./user.js').then((m) => m.userRouter)),
195
+ });
196
+
197
+ export type AppRouter = typeof appRouter;
198
+ ```
199
+
200
+ ## Common Mistakes
201
+
202
+ ### [CRITICAL] Calling initTRPC.create() more than once
203
+
204
+ Wrong:
205
+
206
+ ```ts
207
+ // file: userRouter.ts
208
+ import { initTRPC } from '@trpc/server';
209
+ const t = initTRPC.create();
210
+ export const userRouter = t.router({});
211
+
212
+ // file: postRouter.ts
213
+ import { initTRPC } from '@trpc/server';
214
+ const t2 = initTRPC.create();
215
+ export const postRouter = t2.router({});
216
+ ```
217
+
218
+ Correct:
219
+
220
+ ```ts
221
+ // file: trpc.ts (single file, created once)
222
+ import { initTRPC } from '@trpc/server';
223
+ import type { Context } from './context';
224
+
225
+ const t = initTRPC.context<Context>().create();
226
+
227
+ export const router = t.router;
228
+ export const publicProcedure = t.procedure;
229
+ ```
230
+
231
+ Multiple tRPC instances cause type mismatches and runtime errors when routers from different instances are merged.
232
+
233
+ Source: www/docs/server/routers.md
234
+
235
+ ### [HIGH] Using reserved words as procedure names
236
+
237
+ Wrong:
238
+
239
+ ```ts
240
+ import { publicProcedure, router } from './trpc';
241
+
242
+ const appRouter = router({
243
+ then: publicProcedure.query(() => 'hello'),
244
+ });
245
+ ```
246
+
247
+ Correct:
248
+
249
+ ```ts
250
+ import { publicProcedure, router } from './trpc';
251
+
252
+ const appRouter = router({
253
+ next: publicProcedure.query(() => 'hello'),
254
+ });
255
+ ```
256
+
257
+ Router creation throws if procedure names are "then", "call", or "apply" because these conflict with JavaScript Proxy internals.
258
+
259
+ Source: packages/server/src/unstable-core-do-not-import/router.ts
260
+
261
+ ### [CRITICAL] Importing AppRouter as a value import
262
+
263
+ Wrong:
264
+
265
+ ```ts
266
+ // client.ts
267
+ import { AppRouter } from '../server/router';
268
+ ```
269
+
270
+ Correct:
271
+
272
+ ```ts
273
+ // client.ts
274
+ import type { AppRouter } from '../server/router';
275
+ ```
276
+
277
+ A non-type import pulls the entire server bundle into the client; use `import type` so it is stripped at build time.
278
+
279
+ Source: www/docs/server/routers.md
280
+
281
+ ### [MEDIUM] Creating context without inner/outer split
282
+
283
+ Wrong:
284
+
285
+ ```ts
286
+ import type { CreateExpressContextOptions } from '@trpc/server/adapters/express';
287
+
288
+ export function createContext({ req }: CreateExpressContextOptions) {
289
+ return { db: prisma, user: getUserFromReq(req) };
290
+ }
291
+ ```
292
+
293
+ Correct:
294
+
295
+ ```ts
296
+ import type { CreateExpressContextOptions } from '@trpc/server/adapters/express';
297
+
298
+ export function createContextInner(opts: { user?: User }) {
299
+ return { db: prisma, user: opts.user ?? null };
300
+ }
301
+
302
+ export function createContext({ req }: CreateExpressContextOptions) {
303
+ return createContextInner({ user: getUserFromReq(req) });
304
+ }
305
+ ```
306
+
307
+ Without an inner context factory, server-side callers and tests must construct HTTP request objects to get context.
308
+
309
+ Source: www/docs/server/context.md
310
+
311
+ ### [HIGH] Merging routers with different transformers
312
+
313
+ Wrong:
314
+
315
+ ```ts
316
+ import { initTRPC } from '@trpc/server';
317
+ import superjson from 'superjson';
318
+
319
+ const t1 = initTRPC.create({ transformer: superjson });
320
+ const t2 = initTRPC.create();
321
+
322
+ const router1 = t1.router({ a: t1.procedure.query(() => 'a') });
323
+ const router2 = t2.router({ b: t2.procedure.query(() => 'b') });
324
+
325
+ t1.mergeRouters(router1, router2);
326
+ ```
327
+
328
+ Correct:
329
+
330
+ ```ts
331
+ import { initTRPC } from '@trpc/server';
332
+ import superjson from 'superjson';
333
+
334
+ const t = initTRPC.create({ transformer: superjson });
335
+
336
+ const router1 = t.router({ a: t.procedure.query(() => 'a') });
337
+ const router2 = t.router({ b: t.procedure.query(() => 'b') });
338
+
339
+ t.mergeRouters(router1, router2);
340
+ ```
341
+
342
+ `t.mergeRouters()` throws at runtime if the routers were created with different transformer or errorFormatter configurations.
343
+
344
+ Source: packages/server/src/unstable-core-do-not-import/router.ts
345
+
346
+ ### [CRITICAL] Importing appRouter value into client code
347
+
348
+ Wrong:
349
+
350
+ ```ts
351
+ // client.ts
352
+ import { appRouter } from '../server/router';
353
+
354
+ type AppRouter = typeof appRouter;
355
+ ```
356
+
357
+ Correct:
358
+
359
+ ```ts
360
+ // client.ts
361
+ import type { AppRouter } from '../server/router';
362
+
363
+ // server/router.ts
364
+ export type AppRouter = typeof appRouter;
365
+ ```
366
+
367
+ Importing the appRouter value bundles the entire server into the client, even if you only use `typeof`.
368
+
369
+ Source: www/docs/server/routers.md
370
+
371
+ ## See Also
372
+
373
+ - `middlewares` -- add auth, logging, context extension to procedures
374
+ - `validators` -- add input/output validation with Zod
375
+ - `error-handling` -- throw and format typed errors
376
+ - `server-side-calls` -- call procedures from server code
377
+ - `adapter-standalone` -- mount on Node.js HTTP server
378
+ - `adapter-fetch` -- mount on edge runtimes