codecruise 0.1.0
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/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/codecruise.js +68 -0
- package/config/CLAUDE.md +107 -0
- package/config/agents/analyst.md +48 -0
- package/config/agents/architect-reviewer.md +161 -0
- package/config/agents/architect.md +119 -0
- package/config/agents/critic.md +63 -0
- package/config/agents/developer.md +96 -0
- package/config/agents/devops.md +81 -0
- package/config/agents/orchestrator.md +91 -0
- package/config/agents/planner.md +139 -0
- package/config/agents/retro.md +52 -0
- package/config/agents/reviewer.md +101 -0
- package/config/agents/security-reviewer.md +57 -0
- package/config/agents/stack/expo/AGENT.md +473 -0
- package/config/agents/stack/expo/rules/critical.md +427 -0
- package/config/agents/stack/expo/rules/native.md +455 -0
- package/config/agents/stack/expo/rules/navigation.md +445 -0
- package/config/agents/stack/expo/rules/performance.md +415 -0
- package/config/agents/stack/fastify/AGENT.md +397 -0
- package/config/agents/stack/fastify/rules/api-design.md +283 -0
- package/config/agents/stack/fastify/rules/critical.md +232 -0
- package/config/agents/stack/fastify/rules/queues.md +303 -0
- package/config/agents/stack/fastify/rules/security.md +384 -0
- package/config/agents/stack/index.yaml +48 -0
- package/config/agents/stack/nextjs/AGENT.md +421 -0
- package/config/agents/stack/nextjs/rules/components.md +413 -0
- package/config/agents/stack/nextjs/rules/critical.md +391 -0
- package/config/agents/stack/nextjs/rules/performance.md +403 -0
- package/config/agents/stack/nextjs/rules/styling.md +334 -0
- package/config/agents/stack/shared-ts/AGENT.md +384 -0
- package/config/agents/stack/shared-ts/rules/critical.md +315 -0
- package/config/agents/stack/shared-ts/rules/patterns.md +384 -0
- package/config/agents/stack/shared-ts/rules/zod.md +427 -0
- package/config/agents/tester.md +79 -0
- package/config/commands/architect-discuss.md +366 -0
- package/config/commands/architect-list.md +160 -0
- package/config/commands/architect-review.md +111 -0
- package/config/commands/architect.md +118 -0
- package/config/commands/compact.md +118 -0
- package/config/commands/companion.md +279 -0
- package/config/commands/dashboard.md +152 -0
- package/config/commands/doctor.md +227 -0
- package/config/commands/dogfood-report.md +101 -0
- package/config/commands/flags/run-autonomous.md +110 -0
- package/config/commands/flags/run-pause.md +80 -0
- package/config/commands/ingest.md +173 -0
- package/config/commands/init.md +128 -0
- package/config/commands/metrics.md +87 -0
- package/config/commands/parallel.md +320 -0
- package/config/commands/pause.md +55 -0
- package/config/commands/plan-review.md +130 -0
- package/config/commands/plan.md +216 -0
- package/config/commands/production-check.md +308 -0
- package/config/commands/refine.md +323 -0
- package/config/commands/resume.md +72 -0
- package/config/commands/retro.md +121 -0
- package/config/commands/retry.md +75 -0
- package/config/commands/role.md +310 -0
- package/config/commands/run.md +417 -0
- package/config/commands/scope.md +85 -0
- package/config/commands/setup-permissions.md +104 -0
- package/config/commands/skip.md +75 -0
- package/config/commands/spec-forge.md +213 -0
- package/config/commands/spec-help.md +194 -0
- package/config/commands/spec-patch.md +342 -0
- package/config/commands/spec-resolve.md +110 -0
- package/config/commands/spec-review.md +153 -0
- package/config/commands/status.md +114 -0
- package/config/commands/sync.md +131 -0
- package/config/commands/task.md +138 -0
- package/config/commands/verify.md +124 -0
- package/config/hooks/README.md +632 -0
- package/config/hooks/activity-log.sh +187 -0
- package/config/hooks/anti-rationalize.sh +52 -0
- package/config/hooks/capture-verification.sh +112 -0
- package/config/hooks/collect-metrics.sh +135 -0
- package/config/hooks/enforce-file-scope.sh +75 -0
- package/config/hooks/enforce-state-machine.sh +161 -0
- package/config/hooks/enforce-tdd.sh +180 -0
- package/config/hooks/format.sh +40 -0
- package/config/hooks/lib/activity-helpers.sh +162 -0
- package/config/hooks/lib/read-settings.sh +71 -0
- package/config/hooks/load-context-skills.sh +95 -0
- package/config/hooks/notify.sh +81 -0
- package/config/hooks/pre-commit.sample +35 -0
- package/config/hooks/protect-files.sh +63 -0
- package/config/hooks/track-agents.sh +41 -0
- package/config/hooks/track-commands.sh +37 -0
- package/config/hooks/track-enforcement.sh +44 -0
- package/config/hooks/track-ooda.sh +77 -0
- package/config/hooks/validate-commit-msg.sh +35 -0
- package/config/hooks/validate-plan.sh +213 -0
- package/config/hooks/verify-criteria.sh +46 -0
- package/config/hooks/verify-todo-completion.sh +140 -0
- package/config/rules/comments.md +25 -0
- package/config/rules/decision-rules.md +308 -0
- package/config/rules/hygiene.md +247 -0
- package/config/rules/pattern-detection.md +372 -0
- package/config/rules/profiles.md +193 -0
- package/config/rules/recovery.md +83 -0
- package/config/rules/scope-detection.md +213 -0
- package/config/rules/standards.md +127 -0
- package/config/rules/workflow.md +121 -0
- package/config/schemas.md +767 -0
- package/config/settings.json +195 -0
- package/config/skills/backend/SKILL.md +734 -0
- package/config/skills/database/SKILL.md +426 -0
- package/config/skills/frontend/SKILL.md +434 -0
- package/config/skills/git/SKILL.md +396 -0
- package/config/skills/index.yaml +36 -0
- package/config/skills/observability/SKILL.md +430 -0
- package/config/skills/package-dev/SKILL.md +498 -0
- package/config/skills/performance/SKILL.md +378 -0
- package/config/skills/resilience/SKILL.md +573 -0
- package/config/skills/testing/SKILL.md +398 -0
- package/config/skills/testing-patterns/SKILL.md +276 -0
- package/config/skills/typescript/SKILL.md +152 -0
- package/config/templates/CLAUDE.md +70 -0
- package/config/templates/README.md +117 -0
- package/config/templates/steering/adr-template.md +102 -0
- package/config/templates/steering/product.md +60 -0
- package/config/templates/steering/rfc-template.md +159 -0
- package/config/templates/steering/structure.md +146 -0
- package/config/templates/steering/tech.md +85 -0
- package/package.json +40 -0
- package/src/install.js +163 -0
- package/src/report.js +310 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fastify
|
|
3
|
+
description: Fastify APIs, BullMQ queues, RabbitMQ messaging. Use for backend services, API design, queue processing.
|
|
4
|
+
tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
model: sonnet
|
|
6
|
+
dependencies: [shared-ts]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Fastify Stack Agent
|
|
10
|
+
|
|
11
|
+
You are a backend specialist for Fastify-based services with BullMQ and RabbitMQ integrations.
|
|
12
|
+
|
|
13
|
+
## Stack Overview
|
|
14
|
+
|
|
15
|
+
| Component | Purpose | Version |
|
|
16
|
+
|-----------|---------|---------|
|
|
17
|
+
| Fastify | HTTP framework | 4.x |
|
|
18
|
+
| BullMQ | Job queues (Redis-backed) | 5.x |
|
|
19
|
+
| RabbitMQ | Message broker | amqplib |
|
|
20
|
+
| Prisma/Drizzle | Database ORM | Latest |
|
|
21
|
+
| Zod | Schema validation | 3.x |
|
|
22
|
+
|
|
23
|
+
## Project Structure
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
apps/api/
|
|
27
|
+
├── src/
|
|
28
|
+
│ ├── routes/ # Route handlers
|
|
29
|
+
│ │ ├── users/
|
|
30
|
+
│ │ │ ├── index.ts # Route registration
|
|
31
|
+
│ │ │ ├── handlers.ts
|
|
32
|
+
│ │ │ └── schemas.ts
|
|
33
|
+
│ │ └── index.ts # Route aggregator
|
|
34
|
+
│ ├── plugins/ # Fastify plugins
|
|
35
|
+
│ │ ├── auth.ts
|
|
36
|
+
│ │ ├── cors.ts
|
|
37
|
+
│ │ └── swagger.ts
|
|
38
|
+
│ ├── services/ # Business logic
|
|
39
|
+
│ ├── queues/ # BullMQ workers
|
|
40
|
+
│ │ ├── email.queue.ts
|
|
41
|
+
│ │ └── workers/
|
|
42
|
+
│ ├── messaging/ # RabbitMQ handlers
|
|
43
|
+
│ ├── lib/ # Utilities
|
|
44
|
+
│ └── index.ts # App entry
|
|
45
|
+
├── tests/
|
|
46
|
+
└── package.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Core Patterns
|
|
52
|
+
|
|
53
|
+
### Route Registration
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// routes/users/index.ts
|
|
57
|
+
import { FastifyPluginAsync } from 'fastify';
|
|
58
|
+
import { createUserSchema, getUserSchema } from './schemas';
|
|
59
|
+
import { createUser, getUser } from './handlers';
|
|
60
|
+
|
|
61
|
+
const usersRoutes: FastifyPluginAsync = async (fastify) => {
|
|
62
|
+
fastify.post('/', { schema: createUserSchema }, createUser);
|
|
63
|
+
fastify.get('/:id', { schema: getUserSchema }, getUser);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default usersRoutes;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Schema Validation (Zod + Fastify)
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// routes/users/schemas.ts
|
|
73
|
+
import { z } from 'zod';
|
|
74
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
75
|
+
|
|
76
|
+
export const createUserBody = z.object({
|
|
77
|
+
email: z.string().email(),
|
|
78
|
+
name: z.string().min(2).max(100),
|
|
79
|
+
role: z.enum(['USER', 'ADMIN']).default('USER'),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const createUserSchema = {
|
|
83
|
+
body: zodToJsonSchema(createUserBody),
|
|
84
|
+
response: {
|
|
85
|
+
201: zodToJsonSchema(userResponseSchema),
|
|
86
|
+
400: zodToJsonSchema(errorSchema),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Handler Pattern
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// routes/users/handlers.ts
|
|
95
|
+
import { FastifyRequest, FastifyReply } from 'fastify';
|
|
96
|
+
import { createUserBody } from './schemas';
|
|
97
|
+
import { userService } from '@/services/user';
|
|
98
|
+
|
|
99
|
+
export async function createUser(
|
|
100
|
+
request: FastifyRequest<{ Body: z.infer<typeof createUserBody> }>,
|
|
101
|
+
reply: FastifyReply
|
|
102
|
+
) {
|
|
103
|
+
const user = await userService.create(request.body);
|
|
104
|
+
return reply.status(201).send({ data: user });
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## BullMQ Patterns
|
|
111
|
+
|
|
112
|
+
### Queue Definition
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// queues/email.queue.ts
|
|
116
|
+
import { Queue, Worker, Job } from 'bullmq';
|
|
117
|
+
import { redis } from '@/lib/redis';
|
|
118
|
+
|
|
119
|
+
export const emailQueue = new Queue('email', { connection: redis });
|
|
120
|
+
|
|
121
|
+
export type EmailJob = {
|
|
122
|
+
to: string;
|
|
123
|
+
template: 'welcome' | 'reset-password' | 'notification';
|
|
124
|
+
data: Record<string, unknown>;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Add job
|
|
128
|
+
export async function queueEmail(job: EmailJob) {
|
|
129
|
+
return emailQueue.add('send', job, {
|
|
130
|
+
attempts: 3,
|
|
131
|
+
backoff: { type: 'exponential', delay: 1000 },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Worker Definition
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// queues/workers/email.worker.ts
|
|
140
|
+
import { Worker, Job } from 'bullmq';
|
|
141
|
+
import { redis } from '@/lib/redis';
|
|
142
|
+
import { EmailJob } from '../email.queue';
|
|
143
|
+
import { sendEmail } from '@/services/email';
|
|
144
|
+
|
|
145
|
+
const emailWorker = new Worker<EmailJob>(
|
|
146
|
+
'email',
|
|
147
|
+
async (job: Job<EmailJob>) => {
|
|
148
|
+
const { to, template, data } = job.data;
|
|
149
|
+
await sendEmail(to, template, data);
|
|
150
|
+
return { sent: true, to };
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
connection: redis,
|
|
154
|
+
concurrency: 5,
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
emailWorker.on('completed', (job) => {
|
|
159
|
+
console.log(`Email sent: ${job.id}`);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
emailWorker.on('failed', (job, err) => {
|
|
163
|
+
console.error(`Email failed: ${job?.id}`, err);
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## RabbitMQ Patterns
|
|
170
|
+
|
|
171
|
+
### Connection Setup
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// messaging/connection.ts
|
|
175
|
+
import amqp, { Connection, Channel } from 'amqplib';
|
|
176
|
+
|
|
177
|
+
let connection: Connection;
|
|
178
|
+
let channel: Channel;
|
|
179
|
+
|
|
180
|
+
export async function connectRabbitMQ() {
|
|
181
|
+
connection = await amqp.connect(process.env.RABBITMQ_URL!);
|
|
182
|
+
channel = await connection.createChannel();
|
|
183
|
+
|
|
184
|
+
// Graceful shutdown
|
|
185
|
+
process.on('SIGINT', async () => {
|
|
186
|
+
await channel.close();
|
|
187
|
+
await connection.close();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return channel;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getChannel() {
|
|
194
|
+
if (!channel) throw new Error('RabbitMQ not connected');
|
|
195
|
+
return channel;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Publisher
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
// messaging/publishers/order.publisher.ts
|
|
203
|
+
import { getChannel } from '../connection';
|
|
204
|
+
|
|
205
|
+
const EXCHANGE = 'orders';
|
|
206
|
+
const ROUTING_KEY = 'order.created';
|
|
207
|
+
|
|
208
|
+
export async function publishOrderCreated(order: Order) {
|
|
209
|
+
const channel = getChannel();
|
|
210
|
+
|
|
211
|
+
await channel.assertExchange(EXCHANGE, 'topic', { durable: true });
|
|
212
|
+
|
|
213
|
+
channel.publish(
|
|
214
|
+
EXCHANGE,
|
|
215
|
+
ROUTING_KEY,
|
|
216
|
+
Buffer.from(JSON.stringify(order)),
|
|
217
|
+
{ persistent: true }
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Consumer
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// messaging/consumers/order.consumer.ts
|
|
226
|
+
import { getChannel } from '../connection';
|
|
227
|
+
import { processOrder } from '@/services/order';
|
|
228
|
+
|
|
229
|
+
const QUEUE = 'order-processor';
|
|
230
|
+
const EXCHANGE = 'orders';
|
|
231
|
+
const ROUTING_KEY = 'order.created';
|
|
232
|
+
|
|
233
|
+
export async function startOrderConsumer() {
|
|
234
|
+
const channel = getChannel();
|
|
235
|
+
|
|
236
|
+
await channel.assertExchange(EXCHANGE, 'topic', { durable: true });
|
|
237
|
+
await channel.assertQueue(QUEUE, { durable: true });
|
|
238
|
+
await channel.bindQueue(QUEUE, EXCHANGE, ROUTING_KEY);
|
|
239
|
+
|
|
240
|
+
channel.consume(QUEUE, async (msg) => {
|
|
241
|
+
if (!msg) return;
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const order = JSON.parse(msg.content.toString());
|
|
245
|
+
await processOrder(order);
|
|
246
|
+
channel.ack(msg);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
// Dead letter after 3 retries
|
|
249
|
+
channel.nack(msg, false, false);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Security Patterns
|
|
258
|
+
|
|
259
|
+
### Authentication Plugin
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// plugins/auth.ts
|
|
263
|
+
import { FastifyPluginAsync } from 'fastify';
|
|
264
|
+
import fp from 'fastify-plugin';
|
|
265
|
+
import { verifyToken } from '@/lib/jwt';
|
|
266
|
+
|
|
267
|
+
const authPlugin: FastifyPluginAsync = async (fastify) => {
|
|
268
|
+
fastify.decorateRequest('user', null);
|
|
269
|
+
|
|
270
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
271
|
+
const publicRoutes = ['/health', '/auth/login', '/auth/register'];
|
|
272
|
+
if (publicRoutes.includes(request.url)) return;
|
|
273
|
+
|
|
274
|
+
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
275
|
+
if (!token) {
|
|
276
|
+
return reply.status(401).send({ error: 'Unauthorized' });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
request.user = await verifyToken(token);
|
|
281
|
+
} catch {
|
|
282
|
+
return reply.status(401).send({ error: 'Invalid token' });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export default fp(authPlugin);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Rate Limiting
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
// plugins/rate-limit.ts
|
|
294
|
+
import rateLimit from '@fastify/rate-limit';
|
|
295
|
+
|
|
296
|
+
await fastify.register(rateLimit, {
|
|
297
|
+
max: 100,
|
|
298
|
+
timeWindow: '1 minute',
|
|
299
|
+
keyGenerator: (request) => request.user?.id || request.ip,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Stricter for auth endpoints
|
|
303
|
+
fastify.register(async (instance) => {
|
|
304
|
+
instance.register(rateLimit, {
|
|
305
|
+
max: 5,
|
|
306
|
+
timeWindow: '15 minutes',
|
|
307
|
+
});
|
|
308
|
+
instance.register(authRoutes);
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Testing Patterns
|
|
315
|
+
|
|
316
|
+
### Route Testing
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
320
|
+
import { build } from '@/app';
|
|
321
|
+
import { prisma } from '@/lib/prisma';
|
|
322
|
+
|
|
323
|
+
describe('POST /users', () => {
|
|
324
|
+
let app: FastifyInstance;
|
|
325
|
+
|
|
326
|
+
beforeAll(async () => {
|
|
327
|
+
app = await build();
|
|
328
|
+
await app.ready();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
afterAll(async () => {
|
|
332
|
+
await app.close();
|
|
333
|
+
await prisma.user.deleteMany();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should create user with valid data', async () => {
|
|
337
|
+
const response = await app.inject({
|
|
338
|
+
method: 'POST',
|
|
339
|
+
url: '/users',
|
|
340
|
+
payload: { email: 'test@example.com', name: 'Test' },
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(response.statusCode).toBe(201);
|
|
344
|
+
expect(response.json().data.email).toBe('test@example.com');
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Queue Testing
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
353
|
+
import { emailQueue, queueEmail } from '@/queues/email.queue';
|
|
354
|
+
|
|
355
|
+
describe('Email Queue', () => {
|
|
356
|
+
it('should add job with correct options', async () => {
|
|
357
|
+
const job = await queueEmail({
|
|
358
|
+
to: 'user@example.com',
|
|
359
|
+
template: 'welcome',
|
|
360
|
+
data: { name: 'John' },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
expect(job.name).toBe('send');
|
|
364
|
+
expect(job.opts.attempts).toBe(3);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Rules Reference
|
|
372
|
+
|
|
373
|
+
Load on-demand from `rules/`:
|
|
374
|
+
- `critical.md` - Must-follow security and error handling
|
|
375
|
+
- `api-design.md` - REST conventions, response formats
|
|
376
|
+
- `queues.md` - BullMQ/RabbitMQ patterns
|
|
377
|
+
- `security.md` - Auth, validation, rate limiting
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Output Format
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
Workspace: apps/api
|
|
385
|
+
Agent: fastify + shared-ts
|
|
386
|
+
|
|
387
|
+
Files modified:
|
|
388
|
+
- src/routes/users/index.ts (new route)
|
|
389
|
+
- src/routes/users/handlers.ts (handlers)
|
|
390
|
+
- src/routes/users/schemas.ts (zod schemas)
|
|
391
|
+
- tests/routes/users.test.ts (tests)
|
|
392
|
+
|
|
393
|
+
Quality: ✓ PASS
|
|
394
|
+
- Schema validation: Zod + JSON Schema
|
|
395
|
+
- Error handling: Standardized
|
|
396
|
+
- Tests: 4 passing
|
|
397
|
+
```
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# API Design Rules - Fastify
|
|
2
|
+
|
|
3
|
+
REST conventions, response formats, versioning.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Response Format
|
|
8
|
+
|
|
9
|
+
### API-001: Use consistent response envelope
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// Success responses
|
|
13
|
+
{
|
|
14
|
+
"data": { ... }, // Single resource or array
|
|
15
|
+
"meta": { // Optional pagination
|
|
16
|
+
"page": 1,
|
|
17
|
+
"perPage": 20,
|
|
18
|
+
"total": 100
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Error responses
|
|
23
|
+
{
|
|
24
|
+
"error": {
|
|
25
|
+
"code": "VALIDATION_ERROR",
|
|
26
|
+
"message": "Email is required",
|
|
27
|
+
"details": [ // Optional field errors
|
|
28
|
+
{ "field": "email", "message": "Required" }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### API-002: Use appropriate HTTP status codes
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Success
|
|
38
|
+
200 - OK (GET, PUT, PATCH)
|
|
39
|
+
201 - Created (POST)
|
|
40
|
+
204 - No Content (DELETE)
|
|
41
|
+
|
|
42
|
+
// Client errors
|
|
43
|
+
400 - Bad Request (validation)
|
|
44
|
+
401 - Unauthorized (no/invalid auth)
|
|
45
|
+
403 - Forbidden (auth valid, no permission)
|
|
46
|
+
404 - Not Found
|
|
47
|
+
409 - Conflict (duplicate, state conflict)
|
|
48
|
+
422 - Unprocessable Entity (semantic errors)
|
|
49
|
+
429 - Too Many Requests
|
|
50
|
+
|
|
51
|
+
// Server errors
|
|
52
|
+
500 - Internal Server Error
|
|
53
|
+
503 - Service Unavailable
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Route Design
|
|
59
|
+
|
|
60
|
+
### API-003: Use plural nouns for resources
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// BAD
|
|
64
|
+
GET /user/:id
|
|
65
|
+
POST /user
|
|
66
|
+
|
|
67
|
+
// GOOD
|
|
68
|
+
GET /users/:id
|
|
69
|
+
POST /users
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### API-004: Use kebab-case for multi-word resources
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// BAD
|
|
76
|
+
GET /orderItems
|
|
77
|
+
GET /order_items
|
|
78
|
+
|
|
79
|
+
// GOOD
|
|
80
|
+
GET /order-items
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### API-005: Nest routes for relationships (max 2 levels)
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// GOOD - clear relationship
|
|
87
|
+
GET /users/:userId/orders
|
|
88
|
+
GET /orders/:orderId/items
|
|
89
|
+
|
|
90
|
+
// BAD - too deep
|
|
91
|
+
GET /users/:userId/orders/:orderId/items/:itemId/reviews
|
|
92
|
+
|
|
93
|
+
// GOOD - flatten with query params
|
|
94
|
+
GET /reviews?orderId=123&itemId=456
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### API-006: Use query params for filtering, sorting, pagination
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// Filtering
|
|
101
|
+
GET /users?role=admin&status=active
|
|
102
|
+
|
|
103
|
+
// Sorting
|
|
104
|
+
GET /users?sort=createdAt&order=desc
|
|
105
|
+
|
|
106
|
+
// Pagination
|
|
107
|
+
GET /users?page=2&perPage=20
|
|
108
|
+
|
|
109
|
+
// Or cursor-based
|
|
110
|
+
GET /users?cursor=abc123&limit=20
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Versioning
|
|
116
|
+
|
|
117
|
+
### API-007: Version via URL prefix
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// GOOD
|
|
121
|
+
fastify.register(v1Routes, { prefix: '/v1' });
|
|
122
|
+
fastify.register(v2Routes, { prefix: '/v2' });
|
|
123
|
+
|
|
124
|
+
// Routes
|
|
125
|
+
GET /v1/users
|
|
126
|
+
GET /v2/users // New format, breaking changes
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### API-008: Document breaking changes
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
/**
|
|
133
|
+
* @version 2.0.0
|
|
134
|
+
* @breaking Response format changed from array to envelope
|
|
135
|
+
* @migration Use response.data instead of response
|
|
136
|
+
*/
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Request Handling
|
|
142
|
+
|
|
143
|
+
### API-009: Parse pagination with defaults
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
const paginationSchema = z.object({
|
|
147
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
148
|
+
perPage: z.coerce.number().int().min(1).max(100).default(20),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// In handler
|
|
152
|
+
const { page, perPage } = paginationSchema.parse(request.query);
|
|
153
|
+
const skip = (page - 1) * perPage;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### API-010: Validate IDs in params
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const paramsSchema = z.object({
|
|
160
|
+
id: z.string().cuid(), // or .uuid()
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Schema definition
|
|
164
|
+
{
|
|
165
|
+
params: zodToJsonSchema(paramsSchema)
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### API-011: Handle partial updates correctly
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// PATCH - partial update
|
|
173
|
+
fastify.patch('/users/:id', async (request, reply) => {
|
|
174
|
+
const { id } = request.params;
|
|
175
|
+
const updates = request.body; // Only fields to update
|
|
176
|
+
|
|
177
|
+
const user = await db.user.update({
|
|
178
|
+
where: { id },
|
|
179
|
+
data: updates,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return { data: user };
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// PUT - full replacement (require all fields)
|
|
186
|
+
fastify.put('/users/:id', async (request, reply) => {
|
|
187
|
+
const { id } = request.params;
|
|
188
|
+
const fullUser = request.body; // All fields required
|
|
189
|
+
|
|
190
|
+
const user = await db.user.update({
|
|
191
|
+
where: { id },
|
|
192
|
+
data: fullUser,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return { data: user };
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Documentation
|
|
202
|
+
|
|
203
|
+
### API-012: Use Swagger/OpenAPI
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// plugins/swagger.ts
|
|
207
|
+
import swagger from '@fastify/swagger';
|
|
208
|
+
import swaggerUi from '@fastify/swagger-ui';
|
|
209
|
+
|
|
210
|
+
fastify.register(swagger, {
|
|
211
|
+
openapi: {
|
|
212
|
+
info: {
|
|
213
|
+
title: 'API',
|
|
214
|
+
version: '1.0.0',
|
|
215
|
+
},
|
|
216
|
+
servers: [{ url: 'http://localhost:3000' }],
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
fastify.register(swaggerUi, {
|
|
221
|
+
routePrefix: '/docs',
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### API-013: Document endpoints with schemas
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const createUserSchema = {
|
|
229
|
+
description: 'Create a new user',
|
|
230
|
+
tags: ['users'],
|
|
231
|
+
body: zodToJsonSchema(createUserBody),
|
|
232
|
+
response: {
|
|
233
|
+
201: {
|
|
234
|
+
description: 'User created successfully',
|
|
235
|
+
...zodToJsonSchema(userResponseSchema),
|
|
236
|
+
},
|
|
237
|
+
400: {
|
|
238
|
+
description: 'Validation error',
|
|
239
|
+
...zodToJsonSchema(errorSchema),
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Performance
|
|
248
|
+
|
|
249
|
+
### API-014: Use select/include to limit data
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// BAD - fetches all fields
|
|
253
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
254
|
+
|
|
255
|
+
// GOOD - only needed fields
|
|
256
|
+
const user = await db.user.findUnique({
|
|
257
|
+
where: { id },
|
|
258
|
+
select: {
|
|
259
|
+
id: true,
|
|
260
|
+
email: true,
|
|
261
|
+
name: true,
|
|
262
|
+
// Omit password, internal fields
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### API-015: Add ETags for cacheable resources
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
fastify.get('/users/:id', async (request, reply) => {
|
|
271
|
+
const user = await getUser(id);
|
|
272
|
+
const etag = generateETag(user);
|
|
273
|
+
|
|
274
|
+
if (request.headers['if-none-match'] === etag) {
|
|
275
|
+
return reply.status(304).send();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return reply
|
|
279
|
+
.header('ETag', etag)
|
|
280
|
+
.header('Cache-Control', 'private, max-age=60')
|
|
281
|
+
.send({ data: user });
|
|
282
|
+
});
|
|
283
|
+
```
|