start-vibing 2.0.11 → 2.0.13
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 +177 -177
- package/dist/cli.js +19 -2
- package/package.json +42 -42
- package/template/.claude/CLAUDE.md +174 -174
- package/template/.claude/agents/01-orchestration/agent-selector.md +130 -130
- package/template/.claude/agents/01-orchestration/checkpoint-manager.md +142 -142
- package/template/.claude/agents/01-orchestration/context-manager.md +138 -138
- package/template/.claude/agents/01-orchestration/error-recovery.md +182 -182
- package/template/.claude/agents/01-orchestration/orchestrator.md +114 -114
- package/template/.claude/agents/01-orchestration/parallel-coordinator.md +141 -141
- package/template/.claude/agents/01-orchestration/task-decomposer.md +121 -121
- package/template/.claude/agents/01-orchestration/workflow-router.md +114 -114
- package/template/.claude/agents/02-typescript/bun-runtime-expert.md +197 -197
- package/template/.claude/agents/02-typescript/esm-resolver.md +193 -193
- package/template/.claude/agents/02-typescript/import-alias-enforcer.md +158 -158
- package/template/.claude/agents/02-typescript/ts-generics-helper.md +183 -183
- package/template/.claude/agents/02-typescript/ts-migration-helper.md +238 -238
- package/template/.claude/agents/02-typescript/ts-strict-checker.md +180 -180
- package/template/.claude/agents/02-typescript/ts-types-analyzer.md +199 -199
- package/template/.claude/agents/02-typescript/type-definition-writer.md +187 -187
- package/template/.claude/agents/02-typescript/zod-schema-designer.md +212 -212
- package/template/.claude/agents/02-typescript/zod-validator.md +158 -158
- package/template/.claude/agents/03-testing/playwright-assertions.md +265 -265
- package/template/.claude/agents/03-testing/playwright-e2e.md +247 -247
- package/template/.claude/agents/03-testing/playwright-fixtures.md +234 -234
- package/template/.claude/agents/03-testing/playwright-multi-viewport.md +256 -256
- package/template/.claude/agents/03-testing/playwright-page-objects.md +247 -247
- package/template/.claude/agents/03-testing/test-cleanup-manager.md +248 -248
- package/template/.claude/agents/03-testing/test-data-generator.md +254 -254
- package/template/.claude/agents/03-testing/tester-integration.md +278 -278
- package/template/.claude/agents/03-testing/tester-unit.md +207 -207
- package/template/.claude/agents/03-testing/vitest-config.md +287 -287
- package/template/.claude/agents/04-docker/container-health.md +255 -255
- package/template/.claude/agents/04-docker/deployment-validator.md +225 -225
- package/template/.claude/agents/04-docker/docker-compose-designer.md +281 -281
- package/template/.claude/agents/04-docker/docker-env-manager.md +235 -235
- package/template/.claude/agents/04-docker/docker-multi-stage.md +241 -241
- package/template/.claude/agents/04-docker/dockerfile-optimizer.md +208 -208
- package/template/.claude/agents/05-database/database-seeder.md +273 -273
- package/template/.claude/agents/05-database/mongodb-query-optimizer.md +230 -230
- package/template/.claude/agents/05-database/mongoose-aggregation.md +306 -306
- package/template/.claude/agents/05-database/mongoose-index-optimizer.md +182 -182
- package/template/.claude/agents/05-database/mongoose-schema-designer.md +267 -267
- package/template/.claude/agents/06-security/auth-session-validator.md +68 -68
- package/template/.claude/agents/06-security/input-sanitizer.md +80 -80
- package/template/.claude/agents/06-security/owasp-checker.md +97 -97
- package/template/.claude/agents/06-security/permission-auditor.md +100 -100
- package/template/.claude/agents/06-security/security-auditor.md +84 -84
- package/template/.claude/agents/06-security/sensitive-data-scanner.md +83 -83
- package/template/.claude/agents/07-documentation/api-documenter.md +136 -136
- package/template/.claude/agents/07-documentation/changelog-manager.md +105 -105
- package/template/.claude/agents/07-documentation/documenter.md +76 -76
- package/template/.claude/agents/07-documentation/domain-updater.md +81 -81
- package/template/.claude/agents/07-documentation/jsdoc-generator.md +114 -114
- package/template/.claude/agents/07-documentation/readme-generator.md +135 -135
- package/template/.claude/agents/08-git/branch-manager.md +58 -58
- package/template/.claude/agents/08-git/commit-manager.md +63 -63
- package/template/.claude/agents/08-git/pr-creator.md +76 -76
- package/template/.claude/agents/09-quality/code-reviewer.md +71 -71
- package/template/.claude/agents/09-quality/quality-checker.md +67 -67
- package/template/.claude/agents/10-research/best-practices-finder.md +89 -89
- package/template/.claude/agents/10-research/competitor-analyzer.md +106 -106
- package/template/.claude/agents/10-research/pattern-researcher.md +93 -93
- package/template/.claude/agents/10-research/research-cache-manager.md +76 -76
- package/template/.claude/agents/10-research/research-web.md +98 -98
- package/template/.claude/agents/10-research/tech-evaluator.md +101 -101
- package/template/.claude/agents/11-ui-ux/accessibility-auditor.md +136 -136
- package/template/.claude/agents/11-ui-ux/design-system-enforcer.md +125 -125
- package/template/.claude/agents/11-ui-ux/skeleton-generator.md +118 -118
- package/template/.claude/agents/11-ui-ux/ui-desktop.md +132 -132
- package/template/.claude/agents/11-ui-ux/ui-mobile.md +98 -98
- package/template/.claude/agents/11-ui-ux/ui-tablet.md +110 -110
- package/template/.claude/agents/12-performance/api-latency-analyzer.md +156 -156
- package/template/.claude/agents/12-performance/bundle-analyzer.md +113 -113
- package/template/.claude/agents/12-performance/memory-leak-detector.md +137 -137
- package/template/.claude/agents/12-performance/performance-profiler.md +115 -115
- package/template/.claude/agents/12-performance/query-optimizer.md +124 -124
- package/template/.claude/agents/12-performance/render-optimizer.md +154 -154
- package/template/.claude/agents/13-debugging/build-error-fixer.md +207 -207
- package/template/.claude/agents/13-debugging/debugger.md +149 -149
- package/template/.claude/agents/13-debugging/error-stack-analyzer.md +141 -141
- package/template/.claude/agents/13-debugging/network-debugger.md +208 -208
- package/template/.claude/agents/13-debugging/runtime-error-fixer.md +181 -181
- package/template/.claude/agents/13-debugging/type-error-resolver.md +185 -185
- package/template/.claude/agents/14-validation/final-validator.md +93 -93
- package/template/.claude/agents/_backup/analyzer.md +134 -134
- package/template/.claude/agents/_backup/code-reviewer.md +279 -279
- package/template/.claude/agents/_backup/commit-manager.md +219 -219
- package/template/.claude/agents/_backup/debugger.md +280 -280
- package/template/.claude/agents/_backup/documenter.md +237 -237
- package/template/.claude/agents/_backup/domain-updater.md +197 -197
- package/template/.claude/agents/_backup/final-validator.md +169 -169
- package/template/.claude/agents/_backup/orchestrator.md +149 -149
- package/template/.claude/agents/_backup/performance.md +232 -232
- package/template/.claude/agents/_backup/quality-checker.md +240 -240
- package/template/.claude/agents/_backup/research.md +315 -315
- package/template/.claude/agents/_backup/security-auditor.md +192 -192
- package/template/.claude/agents/_backup/tester.md +566 -566
- package/template/.claude/agents/_backup/ui-ux-reviewer.md +247 -247
- package/template/.claude/config/README.md +30 -30
- package/template/.claude/config/mcp-config.json +344 -344
- package/template/.claude/config/project-config.json +53 -53
- package/template/.claude/config/quality-gates.json +46 -46
- package/template/.claude/config/security-rules.json +45 -45
- package/template/.claude/config/testing-config.json +164 -164
- package/template/.claude/hooks/SETUP.md +126 -126
- package/template/.claude/hooks/run-hook.ts +176 -176
- package/template/.claude/hooks/stop-validator.ts +914 -824
- package/template/.claude/hooks/user-prompt-submit.ts +886 -886
- package/template/.claude/scripts/mcp-quick-install.ts +151 -151
- package/template/.claude/scripts/setup-mcps.ts +651 -651
- package/template/.claude/settings.json +275 -275
- package/template/.claude/skills/bun-runtime/SKILL.md +430 -430
- package/template/.claude/skills/codebase-knowledge/domains/claude-system.md +431 -431
- package/template/.claude/skills/codebase-knowledge/domains/mcp-integration.md +295 -295
- package/template/.claude/skills/debugging-patterns/SKILL.md +485 -485
- package/template/.claude/skills/docker-patterns/SKILL.md +555 -555
- package/template/.claude/skills/git-workflow/SKILL.md +454 -454
- package/template/.claude/skills/mongoose-patterns/SKILL.md +499 -499
- package/template/.claude/skills/nextjs-app-router/SKILL.md +327 -327
- package/template/.claude/skills/performance-patterns/SKILL.md +547 -547
- package/template/.claude/skills/playwright-automation/SKILL.md +438 -438
- package/template/.claude/skills/react-patterns/SKILL.md +389 -389
- package/template/.claude/skills/research-cache/SKILL.md +222 -222
- package/template/.claude/skills/shadcn-ui/SKILL.md +511 -511
- package/template/.claude/skills/tailwind-patterns/SKILL.md +465 -465
- package/template/.claude/skills/test-coverage/SKILL.md +467 -467
- package/template/.claude/skills/trpc-api/SKILL.md +434 -434
- package/template/.claude/skills/typescript-strict/SKILL.md +367 -367
- package/template/.claude/skills/zod-validation/SKILL.md +403 -403
- package/template/CLAUDE.md +117 -117
|
@@ -1,434 +1,434 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: trpc-api
|
|
3
|
-
description: tRPC end-to-end type-safe API patterns. Router setup, procedures, middleware, context, client configuration. Use when building type-safe APIs with tRPC.
|
|
4
|
-
allowed-tools: Read, Write, Edit, Grep, Glob
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# tRPC API - End-to-End Type Safety
|
|
8
|
-
|
|
9
|
-
## Purpose
|
|
10
|
-
|
|
11
|
-
Expert guidance for tRPC v11:
|
|
12
|
-
|
|
13
|
-
- **Router Setup** - Modular router architecture
|
|
14
|
-
- **Procedures** - Queries, mutations, subscriptions
|
|
15
|
-
- **Middleware** - Auth, logging, rate limiting
|
|
16
|
-
- **Context** - Session, database, utilities
|
|
17
|
-
- **Client** - React Query integration
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
## Project Structure
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
server/
|
|
25
|
-
├── trpc/
|
|
26
|
-
│ ├── index.ts # Router exports
|
|
27
|
-
│ ├── trpc.ts # tRPC instance
|
|
28
|
-
│ ├── context.ts # Context creation
|
|
29
|
-
│ └── routers/
|
|
30
|
-
│ ├── user.router.ts
|
|
31
|
-
│ ├── post.router.ts
|
|
32
|
-
│ └── _app.ts # Root router
|
|
33
|
-
app/
|
|
34
|
-
└── api/trpc/[trpc]/route.ts # Next.js handler
|
|
35
|
-
lib/
|
|
36
|
-
└── trpc/
|
|
37
|
-
├── client.ts # tRPC client
|
|
38
|
-
└── react.tsx # React Query provider
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## Server Setup
|
|
44
|
-
|
|
45
|
-
### tRPC Instance
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
// server/trpc/trpc.ts
|
|
49
|
-
import { initTRPC, TRPCError } from '@trpc/server';
|
|
50
|
-
import superjson from 'superjson';
|
|
51
|
-
import { type Context } from './context';
|
|
52
|
-
|
|
53
|
-
const t = initTRPC.context<Context>().create({
|
|
54
|
-
transformer: superjson,
|
|
55
|
-
errorFormatter({ shape, error }) {
|
|
56
|
-
return {
|
|
57
|
-
...shape,
|
|
58
|
-
data: {
|
|
59
|
-
...shape.data,
|
|
60
|
-
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
export const router = t.router;
|
|
67
|
-
export const publicProcedure = t.procedure;
|
|
68
|
-
export const middleware = t.middleware;
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Context
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
// server/trpc/context.ts
|
|
75
|
-
import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
|
|
76
|
-
import { getServerSession } from 'next-auth';
|
|
77
|
-
import { authOptions } from '@/lib/auth';
|
|
78
|
-
import { db } from '@db';
|
|
79
|
-
|
|
80
|
-
export async function createContext(opts: CreateNextContextOptions) {
|
|
81
|
-
const session = await getServerSession(authOptions);
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
session,
|
|
85
|
-
user: session?.user ?? null,
|
|
86
|
-
db,
|
|
87
|
-
req: opts.req,
|
|
88
|
-
res: opts.res,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Auth Middleware
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
// server/trpc/trpc.ts
|
|
99
|
-
const isAuthed = middleware(({ ctx, next }) => {
|
|
100
|
-
if (!ctx.user) {
|
|
101
|
-
throw new TRPCError({
|
|
102
|
-
code: 'UNAUTHORIZED',
|
|
103
|
-
message: 'You must be logged in',
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
return next({
|
|
107
|
-
ctx: {
|
|
108
|
-
...ctx,
|
|
109
|
-
user: ctx.user, // User is now guaranteed
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
export const protectedProcedure = t.procedure.use(isAuthed);
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## Router Patterns
|
|
120
|
-
|
|
121
|
-
### Basic Router
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
// server/trpc/routers/user.router.ts
|
|
125
|
-
import { z } from 'zod';
|
|
126
|
-
import { router, publicProcedure, protectedProcedure } from '../trpc';
|
|
127
|
-
|
|
128
|
-
export const userRouter = router({
|
|
129
|
-
// Query - fetch data
|
|
130
|
-
getById: publicProcedure
|
|
131
|
-
.input(z.object({ id: z.string().uuid() }))
|
|
132
|
-
.query(async ({ input, ctx }) => {
|
|
133
|
-
const user = await ctx.db.user.findUnique({
|
|
134
|
-
where: { id: input.id },
|
|
135
|
-
select: { id: true, name: true, email: true },
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
if (!user) {
|
|
139
|
-
throw new TRPCError({
|
|
140
|
-
code: 'NOT_FOUND',
|
|
141
|
-
message: 'User not found',
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return user;
|
|
146
|
-
}),
|
|
147
|
-
|
|
148
|
-
// Mutation - modify data
|
|
149
|
-
updateProfile: protectedProcedure
|
|
150
|
-
.input(
|
|
151
|
-
z.object({
|
|
152
|
-
name: z.string().min(2).max(100),
|
|
153
|
-
bio: z.string().max(500).optional(),
|
|
154
|
-
})
|
|
155
|
-
)
|
|
156
|
-
.mutation(async ({ input, ctx }) => {
|
|
157
|
-
return ctx.db.user.update({
|
|
158
|
-
where: { id: ctx.user.id },
|
|
159
|
-
data: input,
|
|
160
|
-
});
|
|
161
|
-
}),
|
|
162
|
-
|
|
163
|
-
// Me - current user
|
|
164
|
-
me: protectedProcedure.query(async ({ ctx }) => {
|
|
165
|
-
return ctx.db.user.findUnique({
|
|
166
|
-
where: { id: ctx.user.id },
|
|
167
|
-
});
|
|
168
|
-
}),
|
|
169
|
-
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### Root Router
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
// server/trpc/routers/_app.ts
|
|
176
|
-
import { router } from '../trpc';
|
|
177
|
-
import { userRouter } from './user.router';
|
|
178
|
-
import { postRouter } from './post.router';
|
|
179
|
-
|
|
180
|
-
export const appRouter = router({
|
|
181
|
-
user: userRouter,
|
|
182
|
-
post: postRouter,
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
export type AppRouter = typeof appRouter;
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## Advanced Patterns
|
|
191
|
-
|
|
192
|
-
### Pagination
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
const paginationSchema = z.object({
|
|
196
|
-
cursor: z.string().optional(),
|
|
197
|
-
limit: z.number().min(1).max(100).default(20),
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
export const postRouter = router({
|
|
201
|
-
list: publicProcedure.input(paginationSchema).query(async ({ input, ctx }) => {
|
|
202
|
-
const { cursor, limit } = input;
|
|
203
|
-
|
|
204
|
-
const posts = await ctx.db.post.findMany({
|
|
205
|
-
take: limit + 1,
|
|
206
|
-
cursor: cursor ? { id: cursor } : undefined,
|
|
207
|
-
orderBy: { createdAt: 'desc' },
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
let nextCursor: string | undefined;
|
|
211
|
-
if (posts.length > limit) {
|
|
212
|
-
const nextItem = posts.pop();
|
|
213
|
-
nextCursor = nextItem?.id;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
items: posts,
|
|
218
|
-
nextCursor,
|
|
219
|
-
};
|
|
220
|
-
}),
|
|
221
|
-
});
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### Optimistic Updates
|
|
225
|
-
|
|
226
|
-
```typescript
|
|
227
|
-
// Client-side with React Query
|
|
228
|
-
const utils = trpc.useUtils();
|
|
229
|
-
|
|
230
|
-
const createPost = trpc.post.create.useMutation({
|
|
231
|
-
onMutate: async (newPost) => {
|
|
232
|
-
// Cancel outgoing refetches
|
|
233
|
-
await utils.post.list.cancel();
|
|
234
|
-
|
|
235
|
-
// Snapshot previous value
|
|
236
|
-
const previousPosts = utils.post.list.getData();
|
|
237
|
-
|
|
238
|
-
// Optimistically update
|
|
239
|
-
utils.post.list.setData(undefined, (old) => {
|
|
240
|
-
if (!old) return { items: [newPost], nextCursor: undefined };
|
|
241
|
-
return { ...old, items: [newPost, ...old.items] };
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
return { previousPosts };
|
|
245
|
-
},
|
|
246
|
-
|
|
247
|
-
onError: (err, newPost, context) => {
|
|
248
|
-
// Rollback on error
|
|
249
|
-
utils.post.list.setData(undefined, context?.previousPosts);
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
onSettled: () => {
|
|
253
|
-
// Always refetch after error or success
|
|
254
|
-
utils.post.list.invalidate();
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### Batch Requests
|
|
260
|
-
|
|
261
|
-
```typescript
|
|
262
|
-
// tRPC automatically batches by default
|
|
263
|
-
// Multiple calls in same tick are batched
|
|
264
|
-
|
|
265
|
-
const user = trpc.user.me.useQuery();
|
|
266
|
-
const posts = trpc.post.list.useQuery();
|
|
267
|
-
const notifications = trpc.notification.unread.useQuery();
|
|
268
|
-
// These are batched into a single HTTP request
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
---
|
|
272
|
-
|
|
273
|
-
## Error Handling
|
|
274
|
-
|
|
275
|
-
### Error Codes
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
throw new TRPCError({
|
|
279
|
-
code: 'NOT_FOUND', // 404
|
|
280
|
-
code: 'BAD_REQUEST', // 400
|
|
281
|
-
code: 'UNAUTHORIZED', // 401
|
|
282
|
-
code: 'FORBIDDEN', // 403
|
|
283
|
-
code: 'CONFLICT', // 409
|
|
284
|
-
code: 'INTERNAL_SERVER_ERROR', // 500
|
|
285
|
-
message: 'Descriptive message',
|
|
286
|
-
cause: originalError,
|
|
287
|
-
});
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
### Client Error Handling
|
|
291
|
-
|
|
292
|
-
```typescript
|
|
293
|
-
const mutation = trpc.post.create.useMutation({
|
|
294
|
-
onError: (error) => {
|
|
295
|
-
if (error.data?.code === 'CONFLICT') {
|
|
296
|
-
toast.error('Post with this title already exists');
|
|
297
|
-
} else if (error.data?.zodError) {
|
|
298
|
-
// Validation errors
|
|
299
|
-
const fieldErrors = error.data.zodError.fieldErrors;
|
|
300
|
-
Object.entries(fieldErrors).forEach(([field, errors]) => {
|
|
301
|
-
toast.error(`${field}: ${errors?.join(', ')}`);
|
|
302
|
-
});
|
|
303
|
-
} else {
|
|
304
|
-
toast.error(error.message);
|
|
305
|
-
}
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
---
|
|
311
|
-
|
|
312
|
-
## Client Setup
|
|
313
|
-
|
|
314
|
-
### React Provider
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
// lib/trpc/react.tsx
|
|
318
|
-
'use client';
|
|
319
|
-
|
|
320
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
321
|
-
import { httpBatchLink } from '@trpc/client';
|
|
322
|
-
import { createTRPCReact } from '@trpc/react-query';
|
|
323
|
-
import { useState } from 'react';
|
|
324
|
-
import superjson from 'superjson';
|
|
325
|
-
import { type AppRouter } from '@/server/trpc/routers/_app';
|
|
326
|
-
|
|
327
|
-
export const trpc = createTRPCReact<AppRouter>();
|
|
328
|
-
|
|
329
|
-
export function TRPCProvider({ children }: { children: React.ReactNode }) {
|
|
330
|
-
const [queryClient] = useState(() => new QueryClient({
|
|
331
|
-
defaultOptions: {
|
|
332
|
-
queries: {
|
|
333
|
-
staleTime: 5 * 1000,
|
|
334
|
-
refetchOnWindowFocus: false,
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
}));
|
|
338
|
-
|
|
339
|
-
const [trpcClient] = useState(() =>
|
|
340
|
-
trpc.createClient({
|
|
341
|
-
links: [
|
|
342
|
-
httpBatchLink({
|
|
343
|
-
url: '/api/trpc',
|
|
344
|
-
transformer: superjson,
|
|
345
|
-
}),
|
|
346
|
-
],
|
|
347
|
-
})
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
return (
|
|
351
|
-
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
352
|
-
<QueryClientProvider client={queryClient}>
|
|
353
|
-
{children}
|
|
354
|
-
</QueryClientProvider>
|
|
355
|
-
</trpc.Provider>
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### Usage in Components
|
|
361
|
-
|
|
362
|
-
```typescript
|
|
363
|
-
'use client';
|
|
364
|
-
|
|
365
|
-
import { trpc } from '@/lib/trpc/react';
|
|
366
|
-
|
|
367
|
-
export function UserProfile() {
|
|
368
|
-
const { data: user, isLoading } = trpc.user.me.useQuery();
|
|
369
|
-
const updateProfile = trpc.user.updateProfile.useMutation();
|
|
370
|
-
|
|
371
|
-
if (isLoading) return <Skeleton />;
|
|
372
|
-
if (!user) return null;
|
|
373
|
-
|
|
374
|
-
return (
|
|
375
|
-
<form onSubmit={(e) => {
|
|
376
|
-
e.preventDefault();
|
|
377
|
-
updateProfile.mutate({ name: 'New Name' });
|
|
378
|
-
}}>
|
|
379
|
-
<input defaultValue={user.name} />
|
|
380
|
-
<button type="submit" disabled={updateProfile.isPending}>
|
|
381
|
-
Save
|
|
382
|
-
</button>
|
|
383
|
-
</form>
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
---
|
|
389
|
-
|
|
390
|
-
## Next.js Handler
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
// app/api/trpc/[trpc]/route.ts
|
|
394
|
-
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|
395
|
-
import { appRouter } from '@/server/trpc/routers/_app';
|
|
396
|
-
import { createContext } from '@/server/trpc/context';
|
|
397
|
-
|
|
398
|
-
const handler = (req: Request) =>
|
|
399
|
-
fetchRequestHandler({
|
|
400
|
-
endpoint: '/api/trpc',
|
|
401
|
-
req,
|
|
402
|
-
router: appRouter,
|
|
403
|
-
createContext: () => createContext({ req }),
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
export { handler as GET, handler as POST };
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
---
|
|
410
|
-
|
|
411
|
-
## Agent Integration
|
|
412
|
-
|
|
413
|
-
This skill is used by:
|
|
414
|
-
|
|
415
|
-
- **trpc-expert** subagent
|
|
416
|
-
- **api-documenter** for API documentation
|
|
417
|
-
- **security-auditor** for route validation
|
|
418
|
-
- **test-coverage** for API tests
|
|
419
|
-
|
|
420
|
-
---
|
|
421
|
-
|
|
422
|
-
## FORBIDDEN
|
|
423
|
-
|
|
424
|
-
1. **User ID from input** - ALWAYS use `ctx.user.id`
|
|
425
|
-
2. **Procedures without `.input()`** - Validate all inputs
|
|
426
|
-
3. **`any` in input schemas** - Use proper Zod types
|
|
427
|
-
4. **Sensitive data in responses** - Filter with `.select()`
|
|
428
|
-
5. **Public procedures for mutations** - Use `protectedProcedure`
|
|
429
|
-
|
|
430
|
-
---
|
|
431
|
-
|
|
432
|
-
## Version
|
|
433
|
-
|
|
434
|
-
- **v1.0.0** - Initial implementation based on tRPC v11 patterns
|
|
1
|
+
---
|
|
2
|
+
name: trpc-api
|
|
3
|
+
description: tRPC end-to-end type-safe API patterns. Router setup, procedures, middleware, context, client configuration. Use when building type-safe APIs with tRPC.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# tRPC API - End-to-End Type Safety
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Expert guidance for tRPC v11:
|
|
12
|
+
|
|
13
|
+
- **Router Setup** - Modular router architecture
|
|
14
|
+
- **Procedures** - Queries, mutations, subscriptions
|
|
15
|
+
- **Middleware** - Auth, logging, rate limiting
|
|
16
|
+
- **Context** - Session, database, utilities
|
|
17
|
+
- **Client** - React Query integration
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Project Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
server/
|
|
25
|
+
├── trpc/
|
|
26
|
+
│ ├── index.ts # Router exports
|
|
27
|
+
│ ├── trpc.ts # tRPC instance
|
|
28
|
+
│ ├── context.ts # Context creation
|
|
29
|
+
│ └── routers/
|
|
30
|
+
│ ├── user.router.ts
|
|
31
|
+
│ ├── post.router.ts
|
|
32
|
+
│ └── _app.ts # Root router
|
|
33
|
+
app/
|
|
34
|
+
└── api/trpc/[trpc]/route.ts # Next.js handler
|
|
35
|
+
lib/
|
|
36
|
+
└── trpc/
|
|
37
|
+
├── client.ts # tRPC client
|
|
38
|
+
└── react.tsx # React Query provider
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Server Setup
|
|
44
|
+
|
|
45
|
+
### tRPC Instance
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// server/trpc/trpc.ts
|
|
49
|
+
import { initTRPC, TRPCError } from '@trpc/server';
|
|
50
|
+
import superjson from 'superjson';
|
|
51
|
+
import { type Context } from './context';
|
|
52
|
+
|
|
53
|
+
const t = initTRPC.context<Context>().create({
|
|
54
|
+
transformer: superjson,
|
|
55
|
+
errorFormatter({ shape, error }) {
|
|
56
|
+
return {
|
|
57
|
+
...shape,
|
|
58
|
+
data: {
|
|
59
|
+
...shape.data,
|
|
60
|
+
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const router = t.router;
|
|
67
|
+
export const publicProcedure = t.procedure;
|
|
68
|
+
export const middleware = t.middleware;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Context
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// server/trpc/context.ts
|
|
75
|
+
import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
|
|
76
|
+
import { getServerSession } from 'next-auth';
|
|
77
|
+
import { authOptions } from '@/lib/auth';
|
|
78
|
+
import { db } from '@db';
|
|
79
|
+
|
|
80
|
+
export async function createContext(opts: CreateNextContextOptions) {
|
|
81
|
+
const session = await getServerSession(authOptions);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
session,
|
|
85
|
+
user: session?.user ?? null,
|
|
86
|
+
db,
|
|
87
|
+
req: opts.req,
|
|
88
|
+
res: opts.res,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type Context = Awaited<ReturnType<typeof createContext>>;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Auth Middleware
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// server/trpc/trpc.ts
|
|
99
|
+
const isAuthed = middleware(({ ctx, next }) => {
|
|
100
|
+
if (!ctx.user) {
|
|
101
|
+
throw new TRPCError({
|
|
102
|
+
code: 'UNAUTHORIZED',
|
|
103
|
+
message: 'You must be logged in',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return next({
|
|
107
|
+
ctx: {
|
|
108
|
+
...ctx,
|
|
109
|
+
user: ctx.user, // User is now guaranteed
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const protectedProcedure = t.procedure.use(isAuthed);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Router Patterns
|
|
120
|
+
|
|
121
|
+
### Basic Router
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// server/trpc/routers/user.router.ts
|
|
125
|
+
import { z } from 'zod';
|
|
126
|
+
import { router, publicProcedure, protectedProcedure } from '../trpc';
|
|
127
|
+
|
|
128
|
+
export const userRouter = router({
|
|
129
|
+
// Query - fetch data
|
|
130
|
+
getById: publicProcedure
|
|
131
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
132
|
+
.query(async ({ input, ctx }) => {
|
|
133
|
+
const user = await ctx.db.user.findUnique({
|
|
134
|
+
where: { id: input.id },
|
|
135
|
+
select: { id: true, name: true, email: true },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!user) {
|
|
139
|
+
throw new TRPCError({
|
|
140
|
+
code: 'NOT_FOUND',
|
|
141
|
+
message: 'User not found',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return user;
|
|
146
|
+
}),
|
|
147
|
+
|
|
148
|
+
// Mutation - modify data
|
|
149
|
+
updateProfile: protectedProcedure
|
|
150
|
+
.input(
|
|
151
|
+
z.object({
|
|
152
|
+
name: z.string().min(2).max(100),
|
|
153
|
+
bio: z.string().max(500).optional(),
|
|
154
|
+
})
|
|
155
|
+
)
|
|
156
|
+
.mutation(async ({ input, ctx }) => {
|
|
157
|
+
return ctx.db.user.update({
|
|
158
|
+
where: { id: ctx.user.id },
|
|
159
|
+
data: input,
|
|
160
|
+
});
|
|
161
|
+
}),
|
|
162
|
+
|
|
163
|
+
// Me - current user
|
|
164
|
+
me: protectedProcedure.query(async ({ ctx }) => {
|
|
165
|
+
return ctx.db.user.findUnique({
|
|
166
|
+
where: { id: ctx.user.id },
|
|
167
|
+
});
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Root Router
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// server/trpc/routers/_app.ts
|
|
176
|
+
import { router } from '../trpc';
|
|
177
|
+
import { userRouter } from './user.router';
|
|
178
|
+
import { postRouter } from './post.router';
|
|
179
|
+
|
|
180
|
+
export const appRouter = router({
|
|
181
|
+
user: userRouter,
|
|
182
|
+
post: postRouter,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
export type AppRouter = typeof appRouter;
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Advanced Patterns
|
|
191
|
+
|
|
192
|
+
### Pagination
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const paginationSchema = z.object({
|
|
196
|
+
cursor: z.string().optional(),
|
|
197
|
+
limit: z.number().min(1).max(100).default(20),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
export const postRouter = router({
|
|
201
|
+
list: publicProcedure.input(paginationSchema).query(async ({ input, ctx }) => {
|
|
202
|
+
const { cursor, limit } = input;
|
|
203
|
+
|
|
204
|
+
const posts = await ctx.db.post.findMany({
|
|
205
|
+
take: limit + 1,
|
|
206
|
+
cursor: cursor ? { id: cursor } : undefined,
|
|
207
|
+
orderBy: { createdAt: 'desc' },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
let nextCursor: string | undefined;
|
|
211
|
+
if (posts.length > limit) {
|
|
212
|
+
const nextItem = posts.pop();
|
|
213
|
+
nextCursor = nextItem?.id;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
items: posts,
|
|
218
|
+
nextCursor,
|
|
219
|
+
};
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Optimistic Updates
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Client-side with React Query
|
|
228
|
+
const utils = trpc.useUtils();
|
|
229
|
+
|
|
230
|
+
const createPost = trpc.post.create.useMutation({
|
|
231
|
+
onMutate: async (newPost) => {
|
|
232
|
+
// Cancel outgoing refetches
|
|
233
|
+
await utils.post.list.cancel();
|
|
234
|
+
|
|
235
|
+
// Snapshot previous value
|
|
236
|
+
const previousPosts = utils.post.list.getData();
|
|
237
|
+
|
|
238
|
+
// Optimistically update
|
|
239
|
+
utils.post.list.setData(undefined, (old) => {
|
|
240
|
+
if (!old) return { items: [newPost], nextCursor: undefined };
|
|
241
|
+
return { ...old, items: [newPost, ...old.items] };
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return { previousPosts };
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
onError: (err, newPost, context) => {
|
|
248
|
+
// Rollback on error
|
|
249
|
+
utils.post.list.setData(undefined, context?.previousPosts);
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
onSettled: () => {
|
|
253
|
+
// Always refetch after error or success
|
|
254
|
+
utils.post.list.invalidate();
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Batch Requests
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// tRPC automatically batches by default
|
|
263
|
+
// Multiple calls in same tick are batched
|
|
264
|
+
|
|
265
|
+
const user = trpc.user.me.useQuery();
|
|
266
|
+
const posts = trpc.post.list.useQuery();
|
|
267
|
+
const notifications = trpc.notification.unread.useQuery();
|
|
268
|
+
// These are batched into a single HTTP request
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Error Handling
|
|
274
|
+
|
|
275
|
+
### Error Codes
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
throw new TRPCError({
|
|
279
|
+
code: 'NOT_FOUND', // 404
|
|
280
|
+
code: 'BAD_REQUEST', // 400
|
|
281
|
+
code: 'UNAUTHORIZED', // 401
|
|
282
|
+
code: 'FORBIDDEN', // 403
|
|
283
|
+
code: 'CONFLICT', // 409
|
|
284
|
+
code: 'INTERNAL_SERVER_ERROR', // 500
|
|
285
|
+
message: 'Descriptive message',
|
|
286
|
+
cause: originalError,
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Client Error Handling
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
const mutation = trpc.post.create.useMutation({
|
|
294
|
+
onError: (error) => {
|
|
295
|
+
if (error.data?.code === 'CONFLICT') {
|
|
296
|
+
toast.error('Post with this title already exists');
|
|
297
|
+
} else if (error.data?.zodError) {
|
|
298
|
+
// Validation errors
|
|
299
|
+
const fieldErrors = error.data.zodError.fieldErrors;
|
|
300
|
+
Object.entries(fieldErrors).forEach(([field, errors]) => {
|
|
301
|
+
toast.error(`${field}: ${errors?.join(', ')}`);
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
toast.error(error.message);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Client Setup
|
|
313
|
+
|
|
314
|
+
### React Provider
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// lib/trpc/react.tsx
|
|
318
|
+
'use client';
|
|
319
|
+
|
|
320
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
321
|
+
import { httpBatchLink } from '@trpc/client';
|
|
322
|
+
import { createTRPCReact } from '@trpc/react-query';
|
|
323
|
+
import { useState } from 'react';
|
|
324
|
+
import superjson from 'superjson';
|
|
325
|
+
import { type AppRouter } from '@/server/trpc/routers/_app';
|
|
326
|
+
|
|
327
|
+
export const trpc = createTRPCReact<AppRouter>();
|
|
328
|
+
|
|
329
|
+
export function TRPCProvider({ children }: { children: React.ReactNode }) {
|
|
330
|
+
const [queryClient] = useState(() => new QueryClient({
|
|
331
|
+
defaultOptions: {
|
|
332
|
+
queries: {
|
|
333
|
+
staleTime: 5 * 1000,
|
|
334
|
+
refetchOnWindowFocus: false,
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
}));
|
|
338
|
+
|
|
339
|
+
const [trpcClient] = useState(() =>
|
|
340
|
+
trpc.createClient({
|
|
341
|
+
links: [
|
|
342
|
+
httpBatchLink({
|
|
343
|
+
url: '/api/trpc',
|
|
344
|
+
transformer: superjson,
|
|
345
|
+
}),
|
|
346
|
+
],
|
|
347
|
+
})
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
352
|
+
<QueryClientProvider client={queryClient}>
|
|
353
|
+
{children}
|
|
354
|
+
</QueryClientProvider>
|
|
355
|
+
</trpc.Provider>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Usage in Components
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
'use client';
|
|
364
|
+
|
|
365
|
+
import { trpc } from '@/lib/trpc/react';
|
|
366
|
+
|
|
367
|
+
export function UserProfile() {
|
|
368
|
+
const { data: user, isLoading } = trpc.user.me.useQuery();
|
|
369
|
+
const updateProfile = trpc.user.updateProfile.useMutation();
|
|
370
|
+
|
|
371
|
+
if (isLoading) return <Skeleton />;
|
|
372
|
+
if (!user) return null;
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<form onSubmit={(e) => {
|
|
376
|
+
e.preventDefault();
|
|
377
|
+
updateProfile.mutate({ name: 'New Name' });
|
|
378
|
+
}}>
|
|
379
|
+
<input defaultValue={user.name} />
|
|
380
|
+
<button type="submit" disabled={updateProfile.isPending}>
|
|
381
|
+
Save
|
|
382
|
+
</button>
|
|
383
|
+
</form>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Next.js Handler
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// app/api/trpc/[trpc]/route.ts
|
|
394
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|
395
|
+
import { appRouter } from '@/server/trpc/routers/_app';
|
|
396
|
+
import { createContext } from '@/server/trpc/context';
|
|
397
|
+
|
|
398
|
+
const handler = (req: Request) =>
|
|
399
|
+
fetchRequestHandler({
|
|
400
|
+
endpoint: '/api/trpc',
|
|
401
|
+
req,
|
|
402
|
+
router: appRouter,
|
|
403
|
+
createContext: () => createContext({ req }),
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
export { handler as GET, handler as POST };
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Agent Integration
|
|
412
|
+
|
|
413
|
+
This skill is used by:
|
|
414
|
+
|
|
415
|
+
- **trpc-expert** subagent
|
|
416
|
+
- **api-documenter** for API documentation
|
|
417
|
+
- **security-auditor** for route validation
|
|
418
|
+
- **test-coverage** for API tests
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## FORBIDDEN
|
|
423
|
+
|
|
424
|
+
1. **User ID from input** - ALWAYS use `ctx.user.id`
|
|
425
|
+
2. **Procedures without `.input()`** - Validate all inputs
|
|
426
|
+
3. **`any` in input schemas** - Use proper Zod types
|
|
427
|
+
4. **Sensitive data in responses** - Filter with `.select()`
|
|
428
|
+
5. **Public procedures for mutations** - Use `protectedProcedure`
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Version
|
|
433
|
+
|
|
434
|
+
- **v1.0.0** - Initial implementation based on tRPC v11 patterns
|