@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,177 @@
1
+ ---
2
+ name: adapter-fetch
3
+ description: >
4
+ Deploy tRPC on WinterCG-compliant edge runtimes with fetchRequestHandler() from
5
+ @trpc/server/adapters/fetch. Supports Cloudflare Workers, Deno Deploy, Vercel
6
+ Edge Runtime, Astro, Remix, SolidStart. FetchCreateContextFnOptions provides
7
+ req (Request) and resHeaders (Headers) for context creation. The endpoint option
8
+ must match the URL path prefix where the handler is mounted.
9
+ type: core
10
+ library: trpc
11
+ library_version: '11.14.0'
12
+ requires:
13
+ - server-setup
14
+ sources:
15
+ - www/docs/server/adapters/fetch.mdx
16
+ - examples/cloudflare-workers/
17
+ - examples/deno-deploy/
18
+ ---
19
+
20
+ # tRPC — Adapter: Fetch / Edge Runtimes
21
+
22
+ ## Setup
23
+
24
+ ```ts
25
+ // Cloudflare Worker example
26
+ import { initTRPC } from '@trpc/server';
27
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
28
+ import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';
29
+ import { z } from 'zod';
30
+
31
+ function createContext({ req, resHeaders }: FetchCreateContextFnOptions) {
32
+ const user = req.headers.get('authorization');
33
+ return { user, resHeaders };
34
+ }
35
+ type Context = Awaited<ReturnType<typeof createContext>>;
36
+
37
+ const t = initTRPC.context<Context>().create();
38
+
39
+ const appRouter = t.router({
40
+ greet: t.procedure
41
+ .input(z.object({ name: z.string() }))
42
+ .query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
43
+ });
44
+
45
+ export type AppRouter = typeof appRouter;
46
+
47
+ export default {
48
+ async fetch(request: Request): Promise<Response> {
49
+ return fetchRequestHandler({
50
+ endpoint: '/trpc',
51
+ req: request,
52
+ router: appRouter,
53
+ createContext,
54
+ });
55
+ },
56
+ };
57
+ ```
58
+
59
+ ## Core Patterns
60
+
61
+ ### Cloudflare Workers
62
+
63
+ ```ts
64
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
65
+ import { createContext } from './context';
66
+ import { appRouter } from './router';
67
+
68
+ export default {
69
+ async fetch(request: Request): Promise<Response> {
70
+ return fetchRequestHandler({
71
+ endpoint: '/trpc',
72
+ req: request,
73
+ router: appRouter,
74
+ createContext,
75
+ });
76
+ },
77
+ };
78
+ ```
79
+
80
+ ### Astro SSR
81
+
82
+ ```ts
83
+ // src/pages/trpc/[trpc].ts
84
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
85
+ import type { APIRoute } from 'astro';
86
+ import { createContext } from '../../server/context';
87
+ import { appRouter } from '../../server/router';
88
+
89
+ export const ALL: APIRoute = (opts) => {
90
+ return fetchRequestHandler({
91
+ endpoint: '/trpc',
92
+ req: opts.request,
93
+ router: appRouter,
94
+ createContext,
95
+ });
96
+ };
97
+ ```
98
+
99
+ ### Remix
100
+
101
+ ```ts
102
+ // app/routes/trpc.$trpc.ts
103
+ import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node';
104
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
105
+ import { createContext } from '~/server/context';
106
+ import { appRouter } from '~/server/router';
107
+
108
+ function handleRequest(args: LoaderFunctionArgs | ActionFunctionArgs) {
109
+ return fetchRequestHandler({
110
+ endpoint: '/trpc',
111
+ req: args.request,
112
+ router: appRouter,
113
+ createContext,
114
+ });
115
+ }
116
+
117
+ export const loader = async (args: LoaderFunctionArgs) => handleRequest(args);
118
+ export const action = async (args: ActionFunctionArgs) => handleRequest(args);
119
+ ```
120
+
121
+ ### Deno Deploy
122
+
123
+ ```ts
124
+ import { fetchRequestHandler } from 'npm:@trpc/server/adapters/fetch';
125
+ import { createContext } from './context.ts';
126
+ import { appRouter } from './router.ts';
127
+
128
+ Deno.serve((request) => {
129
+ return fetchRequestHandler({
130
+ endpoint: '/trpc',
131
+ req: request,
132
+ router: appRouter,
133
+ createContext,
134
+ });
135
+ });
136
+ ```
137
+
138
+ ## Common Mistakes
139
+
140
+ ### HIGH Mismatched endpoint path in fetchRequestHandler
141
+
142
+ Wrong:
143
+
144
+ ```ts
145
+ // Handler mounted at /api/trpc/[trpc] but endpoint says /trpc
146
+ fetchRequestHandler({
147
+ endpoint: '/trpc',
148
+ req: request,
149
+ router: appRouter,
150
+ createContext,
151
+ });
152
+ ```
153
+
154
+ Correct:
155
+
156
+ ```ts
157
+ // endpoint must match the actual URL path prefix
158
+ fetchRequestHandler({
159
+ endpoint: '/api/trpc',
160
+ req: request,
161
+ router: appRouter,
162
+ createContext,
163
+ });
164
+ ```
165
+
166
+ The `endpoint` option tells tRPC where to strip the URL prefix to extract the procedure name. If it does not match the actual mount path, all procedures return 404 because the path parsing extracts the wrong procedure name.
167
+
168
+ Source: www/docs/server/adapters/fetch.mdx
169
+
170
+ ## See Also
171
+
172
+ - **server-setup** -- `initTRPC.create()`, router/procedure definition, context
173
+ - **adapter-standalone** -- alternative for Node.js HTTP server
174
+ - **adapter-express** -- alternative when Express middleware ecosystem is needed
175
+ - **adapter-aws-lambda** -- alternative for AWS Lambda deployments
176
+ - Cloudflare Workers docs: https://developers.cloudflare.com/workers/
177
+ - Deno Deploy docs: https://deno.com/deploy/docs
@@ -0,0 +1,184 @@
1
+ ---
2
+ name: adapter-standalone
3
+ description: >
4
+ Mount tRPC on Node.js built-in HTTP server with createHTTPServer() from
5
+ @trpc/server/adapters/standalone, createHTTPHandler() for custom http.createServer,
6
+ createHTTP2Handler() for HTTP/2 with TLS. Configure basePath to slice URL prefix,
7
+ CORS via the cors npm package passed as middleware option. CreateHTTPContextOptions
8
+ provides req and res for context creation.
9
+ type: core
10
+ library: trpc
11
+ library_version: '11.14.0'
12
+ requires:
13
+ - server-setup
14
+ sources:
15
+ - www/docs/server/adapters/standalone.md
16
+ - examples/standalone-server/src/server.ts
17
+ ---
18
+
19
+ # tRPC — Adapter: Standalone
20
+
21
+ ## Setup
22
+
23
+ ```ts
24
+ // server.ts
25
+ import { initTRPC } from '@trpc/server';
26
+ import { createHTTPServer } from '@trpc/server/adapters/standalone';
27
+ import { z } from 'zod';
28
+
29
+ const t = initTRPC.create();
30
+
31
+ const appRouter = t.router({
32
+ greet: t.procedure
33
+ .input(z.object({ name: z.string() }))
34
+ .query(({ input }) => ({ greeting: `Hello, ${input.name}!` })),
35
+ });
36
+
37
+ export type AppRouter = typeof appRouter;
38
+
39
+ createHTTPServer({
40
+ router: appRouter,
41
+ createContext() {
42
+ return {};
43
+ },
44
+ }).listen(3000);
45
+
46
+ console.log('Listening on http://localhost:3000');
47
+ ```
48
+
49
+ ## Core Patterns
50
+
51
+ ### CORS with the cors package
52
+
53
+ ```ts
54
+ import { createHTTPServer } from '@trpc/server/adapters/standalone';
55
+ import cors from 'cors';
56
+ import { createContext } from './context';
57
+ import { appRouter } from './router';
58
+
59
+ createHTTPServer({
60
+ middleware: cors({ origin: 'http://localhost:5173' }),
61
+ router: appRouter,
62
+ createContext,
63
+ }).listen(3000);
64
+ ```
65
+
66
+ Install CORS support: `npm install cors @types/cors`
67
+
68
+ ### Custom HTTP server with createHTTPHandler
69
+
70
+ ```ts
71
+ import { createServer } from 'http';
72
+ import { createHTTPHandler } from '@trpc/server/adapters/standalone';
73
+ import { appRouter } from './router';
74
+
75
+ const handler = createHTTPHandler({
76
+ router: appRouter,
77
+ createContext() {
78
+ return {};
79
+ },
80
+ });
81
+
82
+ createServer((req, res) => {
83
+ if (req.url?.startsWith('/health')) {
84
+ res.writeHead(200);
85
+ res.end('OK');
86
+ return;
87
+ }
88
+ handler(req, res);
89
+ }).listen(3000);
90
+ ```
91
+
92
+ ### basePath for URL prefix stripping
93
+
94
+ ```ts
95
+ import { createServer } from 'http';
96
+ import { createHTTPHandler } from '@trpc/server/adapters/standalone';
97
+ import { appRouter } from './router';
98
+
99
+ const handler = createHTTPHandler({
100
+ router: appRouter,
101
+ basePath: '/trpc/',
102
+ });
103
+
104
+ createServer((req, res) => {
105
+ if (req.url?.startsWith('/trpc/')) {
106
+ return handler(req, res);
107
+ }
108
+ res.statusCode = 404;
109
+ res.end('Not Found');
110
+ }).listen(3000);
111
+ ```
112
+
113
+ The `basePath` option strips the prefix before routing, so `/trpc/greet` resolves to the `greet` procedure.
114
+
115
+ ### HTTP/2 with createHTTP2Handler
116
+
117
+ ```ts
118
+ import http2 from 'http2';
119
+ import { readFileSync } from 'node:fs';
120
+ import { createHTTP2Handler } from '@trpc/server/adapters/standalone';
121
+ import type { CreateHTTP2ContextOptions } from '@trpc/server/adapters/standalone';
122
+ import { appRouter } from './router';
123
+
124
+ const tlsKey = readFileSync('./certs/server.key');
125
+ const tlsCert = readFileSync('./certs/server.crt');
126
+
127
+ async function createContext(opts: CreateHTTP2ContextOptions) {
128
+ return {};
129
+ }
130
+
131
+ const handler = createHTTP2Handler({
132
+ router: appRouter,
133
+ createContext,
134
+ });
135
+
136
+ const server = http2.createSecureServer(
137
+ { key: tlsKey, cert: tlsCert },
138
+ (req, res) => {
139
+ handler(req, res);
140
+ },
141
+ );
142
+
143
+ server.listen(3001);
144
+ ```
145
+
146
+ ## Common Mistakes
147
+
148
+ ### HIGH No CORS configuration for cross-origin requests
149
+
150
+ Wrong:
151
+
152
+ ```ts
153
+ createHTTPServer({
154
+ router: appRouter,
155
+ createContext() {
156
+ return {};
157
+ },
158
+ }).listen(3000);
159
+ ```
160
+
161
+ Correct:
162
+
163
+ ```ts
164
+ import cors from 'cors';
165
+
166
+ createHTTPServer({
167
+ middleware: cors({ origin: 'http://localhost:5173' }),
168
+ router: appRouter,
169
+ createContext() {
170
+ return {};
171
+ },
172
+ }).listen(3000);
173
+ ```
174
+
175
+ The standalone adapter has no CORS handling by default. Cross-origin browser requests fail silently because preflight OPTIONS requests receive no CORS headers.
176
+
177
+ Source: www/docs/server/adapters/standalone.md
178
+
179
+ ## See Also
180
+
181
+ - **server-setup** -- `initTRPC.create()`, router/procedure definition, context
182
+ - **adapter-express** -- alternative adapter when you need Express middleware ecosystem
183
+ - **adapter-fetch** -- alternative adapter for edge/serverless runtimes
184
+ - **subscriptions** -- adding real-time subscriptions to a standalone server
@@ -0,0 +1,342 @@
1
+ ---
2
+ name: auth
3
+ description: >
4
+ Implement JWT/cookie authentication and authorization in tRPC using createContext
5
+ for user extraction, t.middleware with opts.next({ ctx }) for context narrowing to
6
+ non-null user, protectedProcedure base pattern, client-side Authorization headers
7
+ via httpBatchLink headers(), WebSocket connectionParams, and SSE auth via cookies
8
+ or EventSource polyfill custom headers.
9
+ type: composition
10
+ library: trpc
11
+ library_version: '11.14.0'
12
+ requires:
13
+ - server-setup
14
+ - middlewares
15
+ - client-setup
16
+ sources:
17
+ - www/docs/server/authorization.md
18
+ - www/docs/client/headers.md
19
+ - www/docs/client/links/httpSubscriptionLink.md
20
+ - www/docs/server/websockets.md
21
+ ---
22
+
23
+ # tRPC — Auth
24
+
25
+ ## Setup
26
+
27
+ ```ts
28
+ // server/trpc.ts
29
+ import { initTRPC, TRPCError } from '@trpc/server';
30
+ import type { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
31
+
32
+ export async function createContext({ req }: CreateHTTPContextOptions) {
33
+ async function getUserFromHeader() {
34
+ const token = req.headers.authorization?.split(' ')[1];
35
+ if (token) {
36
+ const user = await verifyJwt(token); // your JWT verification
37
+ return user; // e.g. { id: string; name: string; role: string }
38
+ }
39
+ return null;
40
+ }
41
+ return { user: await getUserFromHeader() };
42
+ }
43
+
44
+ export type Context = Awaited<ReturnType<typeof createContext>>;
45
+
46
+ const t = initTRPC.context<Context>().create();
47
+
48
+ export const publicProcedure = t.procedure;
49
+
50
+ export const protectedProcedure = t.procedure.use(
51
+ async function isAuthed(opts) {
52
+ const { ctx } = opts;
53
+ if (!ctx.user) {
54
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
55
+ }
56
+ return opts.next({
57
+ ctx: {
58
+ user: ctx.user, // narrows user to non-null
59
+ },
60
+ });
61
+ },
62
+ );
63
+
64
+ export const router = t.router;
65
+ ```
66
+
67
+ ```ts
68
+ // client/trpc.ts
69
+ import { createTRPCClient, httpBatchLink } from '@trpc/client';
70
+ import type { AppRouter } from '../server/router';
71
+
72
+ let token = '';
73
+ export function setToken(t: string) {
74
+ token = t;
75
+ }
76
+
77
+ export const trpc = createTRPCClient<AppRouter>({
78
+ links: [
79
+ httpBatchLink({
80
+ url: 'http://localhost:3000/trpc',
81
+ headers() {
82
+ return { Authorization: `Bearer ${token}` };
83
+ },
84
+ }),
85
+ ],
86
+ });
87
+ ```
88
+
89
+ ## Core Patterns
90
+
91
+ ### Context narrowing with auth middleware
92
+
93
+ ```ts
94
+ import { initTRPC, TRPCError } from '@trpc/server';
95
+
96
+ type Context = { user: { id: string; role: string } | null };
97
+ const t = initTRPC.context<Context>().create();
98
+
99
+ const isAuthed = t.middleware(async ({ ctx, next }) => {
100
+ if (!ctx.user) {
101
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
102
+ }
103
+ return next({ ctx: { user: ctx.user } });
104
+ });
105
+
106
+ const isAdmin = t.middleware(async ({ ctx, next }) => {
107
+ if (!ctx.user || ctx.user.role !== 'admin') {
108
+ throw new TRPCError({ code: 'FORBIDDEN' });
109
+ }
110
+ return next({ ctx: { user: ctx.user } });
111
+ });
112
+
113
+ export const protectedProcedure = t.procedure.use(isAuthed);
114
+ export const adminProcedure = t.procedure.use(isAdmin);
115
+ ```
116
+
117
+ ### SSE subscription auth with EventSource polyfill
118
+
119
+ ```ts
120
+ import {
121
+ createTRPCClient,
122
+ httpBatchLink,
123
+ httpSubscriptionLink,
124
+ splitLink,
125
+ } from '@trpc/client';
126
+ import { EventSourcePolyfill } from 'event-source-polyfill';
127
+ import type { AppRouter } from '../server/router';
128
+
129
+ const trpc = createTRPCClient<AppRouter>({
130
+ links: [
131
+ splitLink({
132
+ condition: (op) => op.type === 'subscription',
133
+ true: httpSubscriptionLink({
134
+ url: 'http://localhost:3000/trpc',
135
+ EventSource: EventSourcePolyfill,
136
+ eventSourceOptions: async () => {
137
+ return {
138
+ headers: {
139
+ authorization: `Bearer ${getToken()}`,
140
+ },
141
+ };
142
+ },
143
+ }),
144
+ false: httpBatchLink({
145
+ url: 'http://localhost:3000/trpc',
146
+ headers() {
147
+ return { Authorization: `Bearer ${getToken()}` };
148
+ },
149
+ }),
150
+ }),
151
+ ],
152
+ });
153
+ ```
154
+
155
+ ### WebSocket auth with connectionParams
156
+
157
+ ```ts
158
+ // server/context.ts
159
+ import type { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws';
160
+
161
+ export const createContext = async (opts: CreateWSSContextFnOptions) => {
162
+ const token = opts.info.connectionParams?.token;
163
+ const user = token ? await verifyJwt(token) : null;
164
+ return { user };
165
+ };
166
+ ```
167
+
168
+ ```ts
169
+ // client/trpc.ts
170
+ import { createTRPCClient, createWSClient, wsLink } from '@trpc/client';
171
+ import type { AppRouter } from '../server/router';
172
+
173
+ const wsClient = createWSClient({
174
+ url: 'ws://localhost:3001',
175
+ connectionParams: async () => ({
176
+ token: getToken(),
177
+ }),
178
+ });
179
+
180
+ const trpc = createTRPCClient<AppRouter>({
181
+ links: [wsLink({ client: wsClient })],
182
+ });
183
+ ```
184
+
185
+ ### SSE auth with cookies (same domain)
186
+
187
+ ```ts
188
+ import {
189
+ createTRPCClient,
190
+ httpBatchLink,
191
+ httpSubscriptionLink,
192
+ splitLink,
193
+ } from '@trpc/client';
194
+ import type { AppRouter } from '../server/router';
195
+
196
+ const trpc = createTRPCClient<AppRouter>({
197
+ links: [
198
+ splitLink({
199
+ condition: (op) => op.type === 'subscription',
200
+ true: httpSubscriptionLink({
201
+ url: '/api/trpc',
202
+ eventSourceOptions() {
203
+ return { withCredentials: true };
204
+ },
205
+ }),
206
+ false: httpBatchLink({ url: '/api/trpc' }),
207
+ }),
208
+ ],
209
+ });
210
+ ```
211
+
212
+ ## Common Mistakes
213
+
214
+ ### HIGH Not narrowing user type in auth middleware
215
+
216
+ Wrong:
217
+
218
+ ```ts
219
+ const authMiddleware = t.middleware(async ({ ctx, next }) => {
220
+ if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
221
+ return next(); // user still nullable downstream
222
+ });
223
+ ```
224
+
225
+ Correct:
226
+
227
+ ```ts
228
+ const authMiddleware = t.middleware(async ({ ctx, next }) => {
229
+ if (!ctx.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
230
+ return next({ ctx: { user: ctx.user } }); // narrows to non-null
231
+ });
232
+ ```
233
+
234
+ Without `opts.next({ ctx })`, downstream procedures still see `user` as `{ id: string } | null`, requiring redundant null checks.
235
+
236
+ Source: www/docs/server/authorization.md
237
+
238
+ ### HIGH SSE auth via URL query params exposes tokens
239
+
240
+ Wrong:
241
+
242
+ ```ts
243
+ httpSubscriptionLink({
244
+ url: 'http://localhost:3000/trpc',
245
+ connectionParams: async () => ({
246
+ token: 'my-secret-jwt',
247
+ }),
248
+ });
249
+ ```
250
+
251
+ Correct:
252
+
253
+ ```ts
254
+ import { EventSourcePolyfill } from 'event-source-polyfill';
255
+
256
+ httpSubscriptionLink({
257
+ url: 'http://localhost:3000/trpc',
258
+ EventSource: EventSourcePolyfill,
259
+ eventSourceOptions: async () => ({
260
+ headers: { authorization: 'Bearer my-secret-jwt' },
261
+ }),
262
+ });
263
+ ```
264
+
265
+ `connectionParams` are serialized as URL query strings for SSE, exposing tokens in server logs and browser history. Use cookies for same-domain or custom headers via an EventSource polyfill instead.
266
+
267
+ Source: www/docs/client/links/httpSubscriptionLink.md
268
+
269
+ ### MEDIUM Async headers causing stuck isFetching
270
+
271
+ Wrong:
272
+
273
+ ```ts
274
+ httpBatchLink({
275
+ url: '/api/trpc',
276
+ async headers() {
277
+ const token = await refreshToken(); // can race
278
+ return { Authorization: `Bearer ${token}` };
279
+ },
280
+ });
281
+ ```
282
+
283
+ Correct:
284
+
285
+ ```ts
286
+ let cachedToken: string | null = null;
287
+
288
+ async function ensureToken() {
289
+ if (!cachedToken) cachedToken = await refreshToken();
290
+ return cachedToken;
291
+ }
292
+
293
+ httpBatchLink({
294
+ url: '/api/trpc',
295
+ async headers() {
296
+ return { Authorization: `Bearer ${await ensureToken()}` };
297
+ },
298
+ });
299
+ ```
300
+
301
+ When the headers function is async (e.g., refreshing auth tokens), React Query's `isFetching` can get stuck permanently in certain race conditions.
302
+
303
+ Source: https://github.com/trpc/trpc/issues/7001
304
+
305
+ ### HIGH Skipping auth or opening CORS too wide in prototypes
306
+
307
+ Wrong:
308
+
309
+ ```ts
310
+ import cors from 'cors';
311
+
312
+ createHTTPServer({
313
+ middleware: cors(), // origin: '*' by default
314
+ router: appRouter,
315
+ createContext() {
316
+ return {};
317
+ }, // no auth
318
+ }).listen(3000);
319
+ ```
320
+
321
+ Correct:
322
+
323
+ ```ts
324
+ import cors from 'cors';
325
+
326
+ createHTTPServer({
327
+ middleware: cors({ origin: 'https://myapp.com' }),
328
+ router: appRouter,
329
+ createContext,
330
+ }).listen(3000);
331
+ ```
332
+
333
+ Wildcard CORS and missing auth middleware are acceptable only during local development. Always restrict CORS origins and add auth before deploying.
334
+
335
+ Source: maintainer interview
336
+
337
+ ## See Also
338
+
339
+ - **middlewares** -- context narrowing, `.use()`, `.concat()`, base procedure patterns
340
+ - **subscriptions** -- SSE and WebSocket transport setup for authenticated subscriptions
341
+ - **client-setup** -- `createTRPCClient`, link chain, `headers` option
342
+ - **links** -- `splitLink`, `httpSubscriptionLink`, `wsLink` configuration