start-vibing-stacks 2.7.4 → 2.8.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.
@@ -0,0 +1,569 @@
1
+ ---
2
+ name: fastify-api
3
+ version: 1.0.0
4
+ description: Fastify v5 API patterns with end-to-end Zod typing — fastify-type-provider-zod for request/response/OpenAPI inference, @fastify/swagger + @fastify/swagger-ui for the spec UI, error handling with problem+json, plugins/hooks lifecycle, auth, rate-limit, testing via fastify.inject(). Invoke when building or modifying a Fastify server, route, or plugin. For the universal API contract (status codes, problem+json, pagination, versioning), see openapi-design.
5
+ ---
6
+
7
+ # Fastify — Type-Safe APIs with Zod + OpenAPI
8
+
9
+ **Invoke when adding a Fastify route, building a plugin, or wiring auth/middleware.**
10
+
11
+ > Fastify v5 + `fastify-type-provider-zod` is the 2025 pattern: one Zod schema per route doubles as **request validation**, **response validation**, **TypeScript inference**, and **OpenAPI 3.1 spec source**. Single source of truth, no duplication.
12
+
13
+ This skill covers Fastify-specific wiring. For the API design itself (URLs, status codes, error shape, pagination, versioning), see `openapi-design`. For DB security and access patterns, see `postgres-patterns` and `security-baseline`.
14
+
15
+ ---
16
+
17
+ ## 1. Project Structure
18
+
19
+ ```
20
+ src/
21
+ ├── server.ts # bootstrap (Fastify instance + plugins)
22
+ ├── plugins/ # cross-cutting concerns (auth, db, rate-limit)
23
+ │ ├── auth.ts
24
+ │ ├── db.ts
25
+ │ └── error-handler.ts
26
+ ├── routes/ # one file per resource; auto-loaded via @fastify/autoload
27
+ │ ├── users/
28
+ │ │ ├── index.ts # GET /users, POST /users
29
+ │ │ └── [id].ts # GET/PATCH/DELETE /users/:id
30
+ │ └── orders/
31
+ │ └── index.ts
32
+ ├── schemas/ # shared Zod schemas (User, Pagination, Problem)
33
+ └── lib/ # business logic (services, repositories)
34
+ ```
35
+
36
+ Keep route handlers thin: schema + handler + 1–2 service calls. Business logic lives in `lib/`.
37
+
38
+ ---
39
+
40
+ ## 2. Bootstrap
41
+
42
+ ```bash
43
+ bun add fastify zod fastify-type-provider-zod \
44
+ @fastify/autoload @fastify/cors @fastify/helmet \
45
+ @fastify/rate-limit @fastify/swagger @fastify/swagger-ui
46
+ bun add -D @types/node
47
+ ```
48
+
49
+ ```ts
50
+ // src/server.ts
51
+ import Fastify from 'fastify';
52
+ import autoLoad from '@fastify/autoload';
53
+ import {
54
+ serializerCompiler,
55
+ validatorCompiler,
56
+ jsonSchemaTransform,
57
+ type ZodTypeProvider,
58
+ } from 'fastify-type-provider-zod';
59
+ import { join } from 'node:path';
60
+
61
+ export async function buildServer() {
62
+ const app = Fastify({
63
+ logger: {
64
+ level: process.env['LOG_LEVEL'] ?? 'info',
65
+ redact: ['req.headers.authorization', 'req.headers.cookie', '*.password', '*.token'],
66
+ },
67
+ trustProxy: true,
68
+ bodyLimit: 1_048_576, // 1 MiB
69
+ requestIdHeader: 'x-request-id',
70
+ requestIdLogLabel: 'trace_id',
71
+ }).withTypeProvider<ZodTypeProvider>();
72
+
73
+ app.setValidatorCompiler(validatorCompiler);
74
+ app.setSerializerCompiler(serializerCompiler);
75
+
76
+ await app.register(import('@fastify/helmet'));
77
+ await app.register(import('@fastify/cors'), {
78
+ origin: process.env['ALLOWED_ORIGINS']?.split(',') ?? false,
79
+ credentials: true,
80
+ });
81
+ await app.register(import('@fastify/rate-limit'), {
82
+ max: 100,
83
+ timeWindow: '1 minute',
84
+ });
85
+
86
+ await app.register(import('@fastify/swagger'), {
87
+ openapi: {
88
+ openapi: '3.1.0',
89
+ info: { title: 'API', version: '1.0.0' },
90
+ servers: [
91
+ { url: 'http://localhost:3000', description: 'Local' },
92
+ { url: 'https://api.example.com', description: 'Production' },
93
+ ],
94
+ components: {
95
+ securitySchemes: {
96
+ bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
97
+ },
98
+ },
99
+ },
100
+ transform: jsonSchemaTransform,
101
+ });
102
+ await app.register(import('@fastify/swagger-ui'), { routePrefix: '/docs' });
103
+
104
+ await app.register(autoLoad, { dir: join(import.meta.dir, 'plugins') });
105
+ await app.register(autoLoad, { dir: join(import.meta.dir, 'routes') });
106
+
107
+ return app;
108
+ }
109
+
110
+ // src/index.ts
111
+ const app = await buildServer();
112
+ await app.ready();
113
+ app.swagger(); // generate spec
114
+ await app.listen({ port: Number(process.env['PORT'] ?? 3000), host: '0.0.0.0' });
115
+ ```
116
+
117
+ `withTypeProvider<ZodTypeProvider>()` is the line that turns Fastify's request/reply into a fully Zod-typed instance. Without it, you get untyped `request.body` etc.
118
+
119
+ ---
120
+
121
+ ## 3. A Route, End-to-End
122
+
123
+ ```ts
124
+ // src/routes/users/index.ts
125
+ import { z } from 'zod';
126
+ import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
127
+ import { ProblemSchema, ValidationProblemSchema } from '@/schemas/problem';
128
+
129
+ const UserSchema = z.object({
130
+ id: z.string().uuid(),
131
+ email: z.string().email(),
132
+ name: z.string().min(1).max(100),
133
+ createdAt: z.string().datetime(),
134
+ });
135
+
136
+ const CreateUserSchema = z.object({
137
+ email: z.string().email().toLowerCase().trim(),
138
+ name: z.string().min(1).max(100).trim(),
139
+ });
140
+
141
+ const ListQuerySchema = z.object({
142
+ cursor: z.string().optional(),
143
+ limit: z.coerce.number().int().min(1).max(100).default(50),
144
+ });
145
+
146
+ const route: FastifyPluginAsyncZod = async (app) => {
147
+ app.get('/users', {
148
+ schema: {
149
+ tags: ['Users'],
150
+ summary: 'List users',
151
+ operationId: 'listUsers',
152
+ querystring: ListQuerySchema,
153
+ response: {
154
+ 200: z.object({
155
+ data: z.array(UserSchema),
156
+ next_cursor: z.string().nullable(),
157
+ has_more: z.boolean(),
158
+ }),
159
+ 401: ProblemSchema,
160
+ },
161
+ security: [{ bearerAuth: [] }],
162
+ },
163
+ onRequest: [app.requireAuth], // declared in plugins/auth.ts
164
+ handler: async (req) => {
165
+ // req.query is fully typed: { cursor?: string; limit: number }
166
+ const { cursor, limit } = req.query;
167
+ return app.users.list({ cursor, limit });
168
+ },
169
+ });
170
+
171
+ app.post('/users', {
172
+ schema: {
173
+ tags: ['Users'],
174
+ summary: 'Create user',
175
+ operationId: 'createUser',
176
+ body: CreateUserSchema,
177
+ response: {
178
+ 201: UserSchema,
179
+ 409: ProblemSchema,
180
+ 422: ValidationProblemSchema,
181
+ },
182
+ },
183
+ handler: async (req, reply) => {
184
+ const user = await app.users.create(req.body);
185
+ return reply.code(201).header('location', `/users/${user.id}`).send(user);
186
+ },
187
+ });
188
+ };
189
+
190
+ export default route;
191
+ ```
192
+
193
+ What you get for free:
194
+ - Request body / query / params validated by Zod **before** the handler runs (422 on failure)
195
+ - Response **also** validated (catches drift between code and contract — fail fast in dev)
196
+ - TypeScript types inferred — `req.query.limit` is `number`, `req.body.email` is `string`
197
+ - OpenAPI 3.1 spec generated from the same Zod schemas (`/docs` UI works)
198
+ - `operationId` becomes the SDK function name (see `openapi-design` §9)
199
+
200
+ ---
201
+
202
+ ## 4. Error Handling — `application/problem+json`
203
+
204
+ ```ts
205
+ // src/plugins/error-handler.ts
206
+ import fp from 'fastify-plugin';
207
+ import { ZodError } from 'zod';
208
+
209
+ export default fp(async (app) => {
210
+ app.setErrorHandler((err, req, reply) => {
211
+ const traceId = req.id;
212
+
213
+ // Zod validation failure (request side)
214
+ if (err instanceof ZodError || err.code === 'FST_ERR_VALIDATION') {
215
+ const zerr = (err as any).validation ?? err;
216
+ return reply
217
+ .code(422)
218
+ .type('application/problem+json')
219
+ .send({
220
+ type: 'https://api.example.com/problems/validation-error',
221
+ title: 'Validation failed',
222
+ status: 422,
223
+ detail: 'Request body did not pass schema validation.',
224
+ instance: req.url,
225
+ errors: (zerr.issues ?? zerr).map((i: any) => ({
226
+ field: Array.isArray(i.path) ? i.path.join('.') : i.instancePath,
227
+ code: i.code ?? 'invalid',
228
+ message: i.message,
229
+ })),
230
+ trace_id: traceId,
231
+ });
232
+ }
233
+
234
+ // Rate limit
235
+ if (err.statusCode === 429) {
236
+ return reply.code(429).type('application/problem+json').send({
237
+ type: 'https://api.example.com/problems/rate-limited',
238
+ title: 'Too many requests',
239
+ status: 429,
240
+ detail: err.message,
241
+ instance: req.url,
242
+ trace_id: traceId,
243
+ });
244
+ }
245
+
246
+ // Known HTTP errors
247
+ if (err.statusCode && err.statusCode < 500) {
248
+ return reply.code(err.statusCode).type('application/problem+json').send({
249
+ type: `https://api.example.com/problems/${err.code ?? 'error'}`,
250
+ title: err.message,
251
+ status: err.statusCode,
252
+ detail: err.message,
253
+ instance: req.url,
254
+ trace_id: traceId,
255
+ });
256
+ }
257
+
258
+ // 5xx — never leak internals
259
+ req.log.error({ err, trace_id: traceId }, 'Unhandled error');
260
+ return reply.code(500).type('application/problem+json').send({
261
+ type: 'https://api.example.com/problems/internal',
262
+ title: 'Internal Server Error',
263
+ status: 500,
264
+ detail: 'An unexpected error occurred. Reference the trace_id for support.',
265
+ instance: req.url,
266
+ trace_id: traceId,
267
+ });
268
+ });
269
+ });
270
+ ```
271
+
272
+ The shape and rules for `Problem`/`ValidationProblem` come from `openapi-design` §3 — use the same schema in `schemas/problem.ts` and `$ref` it from every route's error responses.
273
+
274
+ ```ts
275
+ // src/schemas/problem.ts
276
+ import { z } from 'zod';
277
+
278
+ export const ProblemSchema = z.object({
279
+ type: z.string().url(),
280
+ title: z.string(),
281
+ status: z.number().int().min(100).max(599),
282
+ detail: z.string().optional(),
283
+ instance: z.string().optional(),
284
+ trace_id: z.string().optional(),
285
+ });
286
+
287
+ export const ValidationProblemSchema = ProblemSchema.extend({
288
+ errors: z.array(
289
+ z.object({
290
+ field: z.string(),
291
+ code: z.string(),
292
+ message: z.string(),
293
+ })
294
+ ),
295
+ });
296
+ ```
297
+
298
+ ---
299
+
300
+ ## 5. Auth — `decorate` + `onRequest` hook
301
+
302
+ ```ts
303
+ // src/plugins/auth.ts
304
+ import fp from 'fastify-plugin';
305
+ import { jwtVerify } from 'jose';
306
+
307
+ declare module 'fastify' {
308
+ interface FastifyRequest {
309
+ user?: { id: string; email: string };
310
+ }
311
+ interface FastifyInstance {
312
+ requireAuth: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
313
+ }
314
+ }
315
+
316
+ export default fp(async (app) => {
317
+ const secret = new TextEncoder().encode(process.env['JWT_SECRET']!);
318
+
319
+ app.decorate('requireAuth', async (req, reply) => {
320
+ const auth = req.headers.authorization;
321
+ if (!auth?.startsWith('Bearer ')) {
322
+ return reply.code(401).type('application/problem+json').send({
323
+ type: 'https://api.example.com/problems/unauthorized',
324
+ title: 'Unauthorized',
325
+ status: 401,
326
+ instance: req.url,
327
+ trace_id: req.id,
328
+ });
329
+ }
330
+ try {
331
+ const { payload } = await jwtVerify(auth.slice(7), secret, {
332
+ algorithms: ['HS256'],
333
+ });
334
+ req.user = { id: payload.sub!, email: payload['email'] as string };
335
+ } catch {
336
+ return reply.code(401).type('application/problem+json').send({
337
+ type: 'https://api.example.com/problems/invalid-token',
338
+ title: 'Invalid token',
339
+ status: 401,
340
+ instance: req.url,
341
+ trace_id: req.id,
342
+ });
343
+ }
344
+ });
345
+ });
346
+ ```
347
+
348
+ **Always** derive user IDs from `req.user.id` (set by the verified JWT), never from `req.body.userId` / `req.params.userId` for ownership checks. See `security-baseline` §A01.
349
+
350
+ For session cookies (first-party browser): `@fastify/cookie` + `@fastify/session` + a server-side store (Redis). Mark cookies `httpOnly`, `secure`, `sameSite: 'lax'` (or `strict`).
351
+
352
+ ---
353
+
354
+ ## 6. Rate Limiting per Route
355
+
356
+ ```ts
357
+ app.post('/auth/login', {
358
+ config: {
359
+ rateLimit: { max: 5, timeWindow: '1 minute' }, // tighter than global
360
+ },
361
+ schema: { /* ... */ },
362
+ handler: async (req, reply) => { /* ... */ },
363
+ });
364
+ ```
365
+
366
+ Apply tighter limits to: login, signup, password reset, 2FA verification, anything that triggers an outbound email/SMS. The global limit catches the rest.
367
+
368
+ ---
369
+
370
+ ## 7. Database Plugin (Postgres example, ORM-agnostic)
371
+
372
+ ```ts
373
+ // src/plugins/db.ts
374
+ import fp from 'fastify-plugin';
375
+ import postgres from 'postgres';
376
+
377
+ declare module 'fastify' {
378
+ interface FastifyInstance {
379
+ db: ReturnType<typeof postgres>;
380
+ }
381
+ }
382
+
383
+ export default fp(async (app) => {
384
+ const sql = postgres(process.env['DATABASE_URL']!, {
385
+ ssl: 'verify-full',
386
+ max: 10, // per-process pool size
387
+ idle_timeout: 30,
388
+ connect_timeout: 10,
389
+ types: { /* ... */ },
390
+ onnotice: () => {}, // silence NOTICE-level
391
+ });
392
+
393
+ app.decorate('db', sql);
394
+ app.addHook('onClose', async () => sql.end({ timeout: 5 }));
395
+ });
396
+ ```
397
+
398
+ For all things Postgres (role separation, RLS, statement_timeout, pool sizing), see `postgres-patterns`. The Fastify side just needs a clean `decorate` + `onClose` shutdown.
399
+
400
+ ---
401
+
402
+ ## 8. Lifecycle Hooks (when to use which)
403
+
404
+ | Hook | When |
405
+ |---|---|
406
+ | `onRequest` | Before body parsing — auth, request-id |
407
+ | `preParsing` | Mutate the incoming stream (rare) |
408
+ | `preValidation` | Mutate body before Zod runs |
409
+ | `preHandler` | After validation, before handler — fine-grained authz |
410
+ | `preSerialization` | Mutate response before serialization |
411
+ | `onSend` | Mutate the serialized payload — set headers |
412
+ | `onResponse` | After response sent — metrics, audit |
413
+ | `onError` | Log error |
414
+ | `onTimeout` | Log slow request |
415
+
416
+ Rule: do auth in `onRequest`, validation in route schema (Fastify runs it automatically), business in `handler`. Anything else should justify itself.
417
+
418
+ ---
419
+
420
+ ## 9. Testing — `app.inject()` (no socket)
421
+
422
+ ```ts
423
+ // tests/users.test.ts
424
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
425
+ import { buildServer } from '@/server';
426
+
427
+ describe('POST /users', () => {
428
+ let app: Awaited<ReturnType<typeof buildServer>>;
429
+
430
+ beforeAll(async () => { app = await buildServer(); await app.ready(); });
431
+ afterAll(async () => { await app.close(); });
432
+
433
+ it('rejects missing email with 422', async () => {
434
+ const res = await app.inject({
435
+ method: 'POST',
436
+ url: '/users',
437
+ payload: { name: 'Alice' },
438
+ });
439
+ expect(res.statusCode).toBe(422);
440
+ expect(res.json().type).toMatch(/validation-error/);
441
+ expect(res.json().errors).toContainEqual(
442
+ expect.objectContaining({ field: 'email', code: 'invalid_type' })
443
+ );
444
+ });
445
+
446
+ it('creates a user and returns 201', async () => {
447
+ const res = await app.inject({
448
+ method: 'POST',
449
+ url: '/users',
450
+ payload: { email: 'alice@example.com', name: 'Alice' },
451
+ });
452
+ expect(res.statusCode).toBe(201);
453
+ expect(res.headers.location).toMatch(/^\/users\//);
454
+ });
455
+ });
456
+ ```
457
+
458
+ `inject` runs the full Fastify pipeline (plugins, hooks, validation, handler, serialization) without opening a socket. Faster and more deterministic than supertest.
459
+
460
+ For integration tests against a real database, spin up Postgres in Docker (Testcontainers) — see `playwright-automation` and the Docker patterns skill.
461
+
462
+ ---
463
+
464
+ ## 10. Logging — pino with redaction
465
+
466
+ Fastify uses pino by default. Configure redaction at boot to never log secrets:
467
+
468
+ ```ts
469
+ const app = Fastify({
470
+ logger: {
471
+ redact: {
472
+ paths: [
473
+ 'req.headers.authorization',
474
+ 'req.headers.cookie',
475
+ 'req.headers["x-api-key"]',
476
+ '*.password',
477
+ '*.token',
478
+ '*.secret',
479
+ '*.apiKey',
480
+ ],
481
+ remove: true,
482
+ },
483
+ },
484
+ });
485
+ ```
486
+
487
+ Production: ship logs to your APM (Datadog, Grafana, Better Stack) via stdout — pino is JSON-by-default, every log aggregator parses it.
488
+
489
+ ---
490
+
491
+ ## 11. Dev / Build / Run
492
+
493
+ ```jsonc
494
+ // package.json
495
+ {
496
+ "scripts": {
497
+ "dev": "bun --hot src/index.ts",
498
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun",
499
+ "start": "NODE_ENV=production bun ./dist/index.js",
500
+ "test": "bun test",
501
+ "typecheck": "tsc --noEmit",
502
+ "spec:export": "bun src/scripts/dump-openapi.ts > openapi.json"
503
+ }
504
+ }
505
+ ```
506
+
507
+ The `spec:export` script calls `app.swagger()` and writes `openapi.json` to disk — commit it; CI fails the build if the regenerated spec differs from the committed file. That's how you keep code and spec in sync.
508
+
509
+ ```ts
510
+ // src/scripts/dump-openapi.ts
511
+ import { buildServer } from '@/server';
512
+ const app = await buildServer();
513
+ await app.ready();
514
+ process.stdout.write(JSON.stringify(app.swagger(), null, 2));
515
+ process.exit(0);
516
+ ```
517
+
518
+ ---
519
+
520
+ ## 12. Production Hardening
521
+
522
+ | Concern | Setting |
523
+ |---|---|
524
+ | Body size | `bodyLimit: 1_048_576` (1 MiB), tighter for JSON-only endpoints |
525
+ | Trust proxy | `trustProxy: true` only behind a real proxy you control (sets `req.ip` from `X-Forwarded-For`) |
526
+ | CORS | Allowlist origins, never `origin: '*'` with credentials |
527
+ | HSTS | `@fastify/helmet` defaults are sane; review CSP for your front-end |
528
+ | TLS termination | At the proxy (Caddy, Nginx, ALB), not in Fastify |
529
+ | Graceful shutdown | `app.close()` on SIGTERM; drain in-flight, close DB pool |
530
+ | Health endpoints | `/healthz` (liveness, no DB) + `/readyz` (readiness, ping DB). Exempt from auth + rate-limit. |
531
+
532
+ ```ts
533
+ // graceful shutdown
534
+ ['SIGINT', 'SIGTERM'].forEach((sig) => {
535
+ process.once(sig, async () => {
536
+ app.log.info({ sig }, 'Shutting down');
537
+ await app.close();
538
+ process.exit(0);
539
+ });
540
+ });
541
+ ```
542
+
543
+ ---
544
+
545
+ ## FORBIDDEN
546
+
547
+ | Pattern | Reason |
548
+ |---|---|
549
+ | `JSON.stringify` your own response, ignoring response schema | Loses Zod validation, OpenAPI spec drifts |
550
+ | Logic in `routes/` files | Move to `lib/services/` — handlers stay thin and testable |
551
+ | Auth check inside the handler | Use `onRequest` hook; cleaner, runs before validation |
552
+ | Returning raw error objects (`{ error: err.message }`) | Use problem+json — see `openapi-design` §3 |
553
+ | `app.use(...)` Express-style middleware | Fastify uses plugins/hooks — different lifecycle, faster |
554
+ | Skipping response validation in dev | Catch contract drift early; opt out only on hot paths in prod via `serializerCompiler` config |
555
+ | Same Zod schema redefined per route | Move to `schemas/`, import — one source of truth |
556
+ | Hand-edited `openapi.json` | Generate from code; commit the artifact |
557
+ | `setRouteValidator` overrides | Use the type provider; overriding kills inference |
558
+ | Forgetting `await app.ready()` before `app.swagger()` | Spec is empty if routes haven't registered |
559
+
560
+ ---
561
+
562
+ ## See Also
563
+
564
+ - `openapi-design` — what the spec should look like (URLs, status codes, problem+json, pagination, versioning, deprecation)
565
+ - `zod-validation` — schema patterns, transforms, env validation
566
+ - `security-baseline` — A01 authz, A02 crypto, A07 auth
567
+ - `api-security-node` — Node-specific hardening (helmet defaults, CORS gotchas, JWT pitfalls)
568
+ - `postgres-patterns` — DB role separation, RLS, statement_timeout, pool sizing
569
+ - `typescript-strict` — TS config that lets the type provider's inference actually help you
@@ -55,7 +55,7 @@
55
55
  "id": "fastify",
56
56
  "name": "Fastify",
57
57
  "icon": "🏎️",
58
- "skills": ["trpc-api"]
58
+ "skills": ["fastify-api", "openapi-design", "trpc-api"]
59
59
  },
60
60
  {
61
61
  "id": "vanilla",
@@ -65,8 +65,8 @@
65
65
  }
66
66
  ],
67
67
  "databases": [
68
- { "id": "mongodb", "name": "MongoDB", "icon": "🍃" },
69
- { "id": "postgresql", "name": "PostgreSQL", "icon": "🐘" },
68
+ { "id": "mongodb", "name": "MongoDB", "icon": "🍃", "skills": ["mongoose-patterns"] },
69
+ { "id": "postgresql", "name": "PostgreSQL", "icon": "🐘", "skills": ["postgres-patterns"] },
70
70
  { "id": "mysql", "name": "MySQL / MariaDB", "icon": "🐬", "default": true },
71
71
  { "id": "sqlite", "name": "SQLite (Turso / libSQL)", "icon": "📁" },
72
72
  { "id": "redis", "name": "Redis (Upstash)", "icon": "🔴" },
@@ -106,7 +106,8 @@
106
106
  "typescript-strict",
107
107
  "bun-runtime",
108
108
  "zod-validation",
109
- "api-security-node"
109
+ "api-security-node",
110
+ "openapi-design"
110
111
  ],
111
112
  "requirements": [
112
113
  {
@@ -45,7 +45,7 @@
45
45
  ],
46
46
  "databases": [
47
47
  { "id": "mysql", "name": "MySQL / MariaDB", "icon": "🐬", "default": true },
48
- { "id": "postgresql", "name": "PostgreSQL", "icon": "🐘" },
48
+ { "id": "postgresql", "name": "PostgreSQL", "icon": "🐘", "skills": ["postgres-patterns"] },
49
49
  { "id": "sqlite", "name": "SQLite", "icon": "📁" }
50
50
  ],
51
51
  "frontendOptions": [
@@ -83,7 +83,8 @@
83
83
  "phpstan-analysis",
84
84
  "composer-workflow",
85
85
  "security-scan-php",
86
- "api-design"
86
+ "api-design",
87
+ "openapi-design"
87
88
  ],
88
89
  "requirements": [
89
90
  {
@@ -27,21 +27,21 @@
27
27
  "icon": "⚡",
28
28
  "detectFiles": ["app/main.py"],
29
29
  "default": true,
30
- "skills": ["fastapi-patterns", "async-patterns"]
30
+ "skills": ["fastapi-patterns", "async-patterns", "openapi-design"]
31
31
  },
32
32
  {
33
33
  "id": "django",
34
34
  "name": "Django 5+",
35
35
  "icon": "🎸",
36
36
  "detectFiles": ["manage.py"],
37
- "skills": ["django-patterns"]
37
+ "skills": ["django-patterns", "openapi-design"]
38
38
  },
39
39
  {
40
40
  "id": "flask",
41
41
  "name": "Flask",
42
42
  "icon": "🧪",
43
43
  "detectFiles": ["app.py", "wsgi.py"],
44
- "skills": []
44
+ "skills": ["openapi-design"]
45
45
  },
46
46
  {
47
47
  "id": "scripts",
@@ -52,7 +52,7 @@
52
52
  ],
53
53
  "databases": [
54
54
  { "id": "mysql", "name": "MySQL / MariaDB", "icon": "🐬", "default": true },
55
- { "id": "postgresql", "name": "PostgreSQL", "icon": "🐘" },
55
+ { "id": "postgresql", "name": "PostgreSQL", "icon": "🐘", "skills": ["postgres-patterns"] },
56
56
  { "id": "sqlite", "name": "SQLite (local)", "icon": "📦" },
57
57
  { "id": "mongodb", "name": "MongoDB", "icon": "🍃" },
58
58
  { "id": "none", "name": "No database", "icon": "⬜" }