@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.
- package/README.md +8 -0
- package/bin/intent.js +20 -0
- package/dist/adapters/next-app-dir.cjs +76 -76
- package/dist/adapters/next-app-dir.mjs +76 -76
- package/dist/adapters/next-app-dir.mjs.map +1 -1
- package/package.json +13 -3
- package/skills/adapter-aws-lambda/SKILL.md +188 -0
- package/skills/adapter-express/SKILL.md +152 -0
- package/skills/adapter-fastify/SKILL.md +206 -0
- package/skills/adapter-fetch/SKILL.md +177 -0
- package/skills/adapter-standalone/SKILL.md +184 -0
- package/skills/auth/SKILL.md +342 -0
- package/skills/caching/SKILL.md +205 -0
- package/skills/error-handling/SKILL.md +253 -0
- package/skills/middlewares/SKILL.md +242 -0
- package/skills/non-json-content-types/SKILL.md +265 -0
- package/skills/server-setup/SKILL.md +378 -0
- package/skills/server-side-calls/SKILL.md +249 -0
- package/skills/service-oriented-architecture/SKILL.md +247 -0
- package/skills/subscriptions/SKILL.md +406 -0
- package/skills/trpc-router/SKILL.md +151 -0
- package/skills/validators/SKILL.md +228 -0
|
@@ -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
|