elsabro 2.1.0 → 2.3.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/agents/elsabro-orchestrator.md +113 -0
- package/commands/elsabro/add-phase.md +17 -0
- package/commands/elsabro/add-todo.md +111 -53
- package/commands/elsabro/audit-milestone.md +19 -0
- package/commands/elsabro/check-todos.md +210 -31
- package/commands/elsabro/complete-milestone.md +20 -1
- package/commands/elsabro/debug.md +19 -0
- package/commands/elsabro/discuss-phase.md +18 -1
- package/commands/elsabro/execute.md +511 -58
- package/commands/elsabro/insert-phase.md +18 -1
- package/commands/elsabro/list-phase-assumptions.md +17 -0
- package/commands/elsabro/new-milestone.md +19 -0
- package/commands/elsabro/new.md +19 -0
- package/commands/elsabro/pause-work.md +19 -0
- package/commands/elsabro/plan-milestone-gaps.md +20 -1
- package/commands/elsabro/plan.md +264 -36
- package/commands/elsabro/progress.md +203 -79
- package/commands/elsabro/quick.md +19 -0
- package/commands/elsabro/remove-phase.md +17 -0
- package/commands/elsabro/research-phase.md +18 -1
- package/commands/elsabro/resume-work.md +19 -0
- package/commands/elsabro/start.md +399 -98
- package/commands/elsabro/verify-work.md +138 -5
- package/hooks/confirm-destructive.sh +145 -0
- package/hooks/hooks-config.json +81 -0
- package/hooks/lint-check.sh +238 -0
- package/hooks/post-edit-test.sh +189 -0
- package/package.json +3 -2
- package/references/SYSTEM_INDEX.md +241 -0
- package/references/command-flow.md +352 -0
- package/references/enforcement-rules.md +331 -0
- package/references/error-contracts-tests.md +1171 -0
- package/references/error-contracts.md +3102 -0
- package/references/error-handling-instructions.md +26 -12
- package/references/parallel-worktrees.md +293 -0
- package/references/state-sync.md +381 -0
- package/references/task-dispatcher.md +388 -0
- package/references/tasks-integration.md +380 -0
- package/scripts/setup-parallel-worktrees.sh +319 -0
- package/skills/api-microservice.md +765 -0
- package/skills/api-setup.md +76 -3
- package/skills/auth-setup.md +46 -6
- package/skills/chrome-extension.md +584 -0
- package/skills/cicd-setup.md +1206 -0
- package/skills/cli-tool.md +884 -0
- package/skills/database-setup.md +41 -5
- package/skills/desktop-app.md +1351 -0
- package/skills/expo-app.md +35 -2
- package/skills/full-stack-app.md +543 -0
- package/skills/memory-update.md +207 -0
- package/skills/mobile-app.md +813 -0
- package/skills/nextjs-app.md +33 -2
- package/skills/payments-setup.md +76 -1
- package/skills/review.md +331 -0
- package/skills/saas-starter.md +639 -0
- package/skills/sentry-setup.md +41 -7
- package/skills/techdebt.md +289 -0
- package/skills/testing-setup.md +1218 -0
- package/skills/tutor.md +219 -0
- package/templates/.planning/notes/.gitkeep +0 -0
- package/templates/CLAUDE.md.template +48 -0
- package/templates/error-handling-config.json +79 -2
- package/templates/mistakes.md.template +52 -0
- package/templates/patterns.md.template +114 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-microservice
|
|
3
|
+
description: Crear microservicio API con Node.js, Fastify, OpenAPI/Swagger, Docker, Prisma, rate limiting y health checks.
|
|
4
|
+
tags: [api, microservice, fastify, docker, openapi, swagger, nodejs, prisma]
|
|
5
|
+
difficulty: intermediate
|
|
6
|
+
estimated_time: 40min
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: API Microservice
|
|
10
|
+
|
|
11
|
+
<when_to_use>
|
|
12
|
+
Usar cuando el usuario menciona:
|
|
13
|
+
- "crear API"
|
|
14
|
+
- "microservicio"
|
|
15
|
+
- "backend API"
|
|
16
|
+
- "REST API"
|
|
17
|
+
- "API con documentacion"
|
|
18
|
+
</when_to_use>
|
|
19
|
+
|
|
20
|
+
<pre_requisites>
|
|
21
|
+
## Pre-requisitos
|
|
22
|
+
|
|
23
|
+
- Node.js 20+
|
|
24
|
+
- Docker Desktop (opcional, para containerization)
|
|
25
|
+
- PostgreSQL (local o Neon/Supabase)
|
|
26
|
+
</pre_requisites>
|
|
27
|
+
|
|
28
|
+
<project_structure>
|
|
29
|
+
## Estructura de Proyecto
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
my-api/
|
|
33
|
+
├── src/
|
|
34
|
+
│ ├── routes/
|
|
35
|
+
│ │ ├── health.ts # Health check endpoints
|
|
36
|
+
│ │ ├── users.ts # User CRUD
|
|
37
|
+
│ │ └── index.ts # Route registration
|
|
38
|
+
│ ├── schemas/
|
|
39
|
+
│ │ └── user.ts # Zod schemas
|
|
40
|
+
│ ├── services/
|
|
41
|
+
│ │ └── userService.ts # Business logic
|
|
42
|
+
│ ├── middleware/
|
|
43
|
+
│ │ ├── auth.ts # JWT auth
|
|
44
|
+
│ │ └── error-handler.ts # Global error handler
|
|
45
|
+
│ ├── plugins/
|
|
46
|
+
│ │ ├── prisma.ts # Prisma plugin
|
|
47
|
+
│ │ └── swagger.ts # Swagger plugin
|
|
48
|
+
│ ├── config/
|
|
49
|
+
│ │ └── env.ts # Environment validation
|
|
50
|
+
│ ├── app.ts # Fastify app builder
|
|
51
|
+
│ └── index.ts # Entry point
|
|
52
|
+
├── prisma/
|
|
53
|
+
│ └── schema.prisma
|
|
54
|
+
├── tests/
|
|
55
|
+
│ └── routes/
|
|
56
|
+
│ └── users.test.ts
|
|
57
|
+
├── Dockerfile
|
|
58
|
+
├── docker-compose.yml
|
|
59
|
+
├── .env
|
|
60
|
+
├── .env.example
|
|
61
|
+
├── tsconfig.json
|
|
62
|
+
└── package.json
|
|
63
|
+
```
|
|
64
|
+
</project_structure>
|
|
65
|
+
|
|
66
|
+
<setup_steps>
|
|
67
|
+
## Pasos de Setup
|
|
68
|
+
|
|
69
|
+
### Paso 1: Inicializar proyecto
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
mkdir my-api && cd my-api
|
|
73
|
+
npm init -y
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Paso 2: Instalar dependencias
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install fastify @fastify/cors @fastify/helmet @fastify/rate-limit @fastify/swagger @fastify/swagger-ui @fastify/jwt
|
|
80
|
+
npm install prisma @prisma/client zod dotenv
|
|
81
|
+
npm install -D typescript tsx @types/node vitest
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Paso 3: Configurar TypeScript
|
|
85
|
+
|
|
86
|
+
Crear `tsconfig.json`:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"compilerOptions": {
|
|
91
|
+
"target": "ES2022",
|
|
92
|
+
"module": "NodeNext",
|
|
93
|
+
"moduleResolution": "NodeNext",
|
|
94
|
+
"outDir": "./dist",
|
|
95
|
+
"rootDir": "./src",
|
|
96
|
+
"strict": true,
|
|
97
|
+
"esModuleInterop": true,
|
|
98
|
+
"skipLibCheck": true,
|
|
99
|
+
"forceConsistentCasingInFileNames": true,
|
|
100
|
+
"resolveJsonModule": true,
|
|
101
|
+
"declaration": true
|
|
102
|
+
},
|
|
103
|
+
"include": ["src/**/*"],
|
|
104
|
+
"exclude": ["node_modules", "dist"]
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Paso 4: Configurar variables de entorno
|
|
109
|
+
|
|
110
|
+
Crear `src/config/env.ts`:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { z } from "zod";
|
|
114
|
+
import "dotenv/config";
|
|
115
|
+
|
|
116
|
+
const envSchema = z.object({
|
|
117
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
118
|
+
PORT: z.coerce.number().default(3000),
|
|
119
|
+
HOST: z.string().default("0.0.0.0"),
|
|
120
|
+
DATABASE_URL: z.string(),
|
|
121
|
+
JWT_SECRET: z.string().min(32),
|
|
122
|
+
RATE_LIMIT_MAX: z.coerce.number().default(100),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const env = envSchema.parse(process.env);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Crear `.env`:
|
|
129
|
+
|
|
130
|
+
```env
|
|
131
|
+
NODE_ENV=development
|
|
132
|
+
PORT=3000
|
|
133
|
+
HOST=0.0.0.0
|
|
134
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/myapi
|
|
135
|
+
JWT_SECRET=your-super-secret-key-at-least-32-characters
|
|
136
|
+
RATE_LIMIT_MAX=100
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Paso 5: Configurar Prisma
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx prisma init
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Editar `prisma/schema.prisma`:
|
|
146
|
+
|
|
147
|
+
```prisma
|
|
148
|
+
generator client {
|
|
149
|
+
provider = "prisma-client-js"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
datasource db {
|
|
153
|
+
provider = "postgresql"
|
|
154
|
+
url = env("DATABASE_URL")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
model User {
|
|
158
|
+
id String @id @default(cuid())
|
|
159
|
+
email String @unique
|
|
160
|
+
name String?
|
|
161
|
+
password String
|
|
162
|
+
role Role @default(USER)
|
|
163
|
+
createdAt DateTime @default(now())
|
|
164
|
+
updatedAt DateTime @updatedAt
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
enum Role {
|
|
168
|
+
USER
|
|
169
|
+
ADMIN
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Paso 6: Crear plugin de Prisma
|
|
174
|
+
|
|
175
|
+
Crear `src/plugins/prisma.ts`:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { FastifyPluginAsync } from "fastify";
|
|
179
|
+
import fp from "fastify-plugin";
|
|
180
|
+
import { PrismaClient } from "@prisma/client";
|
|
181
|
+
|
|
182
|
+
declare module "fastify" {
|
|
183
|
+
interface FastifyInstance {
|
|
184
|
+
prisma: PrismaClient;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const prismaPlugin: FastifyPluginAsync = async (fastify) => {
|
|
189
|
+
const prisma = new PrismaClient({
|
|
190
|
+
log: fastify.log.level === "debug" ? ["query", "error", "warn"] : ["error"],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await prisma.$connect();
|
|
194
|
+
|
|
195
|
+
fastify.decorate("prisma", prisma);
|
|
196
|
+
|
|
197
|
+
fastify.addHook("onClose", async (server) => {
|
|
198
|
+
await server.prisma.$disconnect();
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export default fp(prismaPlugin, { name: "prisma" });
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Paso 7: Crear plugin de Swagger
|
|
206
|
+
|
|
207
|
+
Crear `src/plugins/swagger.ts`:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { FastifyPluginAsync } from "fastify";
|
|
211
|
+
import fp from "fastify-plugin";
|
|
212
|
+
import swagger from "@fastify/swagger";
|
|
213
|
+
import swaggerUi from "@fastify/swagger-ui";
|
|
214
|
+
|
|
215
|
+
const swaggerPlugin: FastifyPluginAsync = async (fastify) => {
|
|
216
|
+
await fastify.register(swagger, {
|
|
217
|
+
openapi: {
|
|
218
|
+
info: {
|
|
219
|
+
title: "My API",
|
|
220
|
+
description: "API documentation",
|
|
221
|
+
version: "1.0.0",
|
|
222
|
+
},
|
|
223
|
+
servers: [
|
|
224
|
+
{
|
|
225
|
+
url: "http://localhost:3000",
|
|
226
|
+
description: "Development server",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
components: {
|
|
230
|
+
securitySchemes: {
|
|
231
|
+
bearerAuth: {
|
|
232
|
+
type: "http",
|
|
233
|
+
scheme: "bearer",
|
|
234
|
+
bearerFormat: "JWT",
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await fastify.register(swaggerUi, {
|
|
242
|
+
routePrefix: "/docs",
|
|
243
|
+
uiConfig: {
|
|
244
|
+
docExpansion: "list",
|
|
245
|
+
deepLinking: false,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export default fp(swaggerPlugin, { name: "swagger" });
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Paso 8: Crear schemas con Zod
|
|
254
|
+
|
|
255
|
+
Crear `src/schemas/user.ts`:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { z } from "zod";
|
|
259
|
+
|
|
260
|
+
export const createUserSchema = z.object({
|
|
261
|
+
email: z.string().email(),
|
|
262
|
+
name: z.string().min(2).optional(),
|
|
263
|
+
password: z.string().min(8),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
export const updateUserSchema = z.object({
|
|
267
|
+
name: z.string().min(2).optional(),
|
|
268
|
+
email: z.string().email().optional(),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
export const userResponseSchema = z.object({
|
|
272
|
+
id: z.string(),
|
|
273
|
+
email: z.string(),
|
|
274
|
+
name: z.string().nullable(),
|
|
275
|
+
role: z.enum(["USER", "ADMIN"]),
|
|
276
|
+
createdAt: z.date(),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
export type CreateUserInput = z.infer<typeof createUserSchema>;
|
|
280
|
+
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
|
281
|
+
export type UserResponse = z.infer<typeof userResponseSchema>;
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Paso 9: Crear rutas de health
|
|
285
|
+
|
|
286
|
+
Crear `src/routes/health.ts`:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { FastifyPluginAsync } from "fastify";
|
|
290
|
+
|
|
291
|
+
export const healthRoutes: FastifyPluginAsync = async (fastify) => {
|
|
292
|
+
fastify.get(
|
|
293
|
+
"/health",
|
|
294
|
+
{
|
|
295
|
+
schema: {
|
|
296
|
+
description: "Health check endpoint",
|
|
297
|
+
tags: ["Health"],
|
|
298
|
+
response: {
|
|
299
|
+
200: {
|
|
300
|
+
type: "object",
|
|
301
|
+
properties: {
|
|
302
|
+
status: { type: "string" },
|
|
303
|
+
timestamp: { type: "string" },
|
|
304
|
+
uptime: { type: "number" },
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
async () => {
|
|
311
|
+
return {
|
|
312
|
+
status: "healthy",
|
|
313
|
+
timestamp: new Date().toISOString(),
|
|
314
|
+
uptime: process.uptime(),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
fastify.get(
|
|
320
|
+
"/health/ready",
|
|
321
|
+
{
|
|
322
|
+
schema: {
|
|
323
|
+
description: "Readiness probe",
|
|
324
|
+
tags: ["Health"],
|
|
325
|
+
response: {
|
|
326
|
+
200: {
|
|
327
|
+
type: "object",
|
|
328
|
+
properties: {
|
|
329
|
+
status: { type: "string" },
|
|
330
|
+
database: { type: "string" },
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
async () => {
|
|
337
|
+
try {
|
|
338
|
+
await fastify.prisma.$queryRaw`SELECT 1`;
|
|
339
|
+
return { status: "ready", database: "connected" };
|
|
340
|
+
} catch {
|
|
341
|
+
return { status: "not ready", database: "disconnected" };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Paso 10: Crear rutas de usuarios
|
|
349
|
+
|
|
350
|
+
Crear `src/routes/users.ts`:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { FastifyPluginAsync } from "fastify";
|
|
354
|
+
import { createUserSchema, updateUserSchema } from "../schemas/user";
|
|
355
|
+
import bcrypt from "bcryptjs";
|
|
356
|
+
|
|
357
|
+
export const userRoutes: FastifyPluginAsync = async (fastify) => {
|
|
358
|
+
// GET /users
|
|
359
|
+
fastify.get(
|
|
360
|
+
"/",
|
|
361
|
+
{
|
|
362
|
+
schema: {
|
|
363
|
+
description: "List all users",
|
|
364
|
+
tags: ["Users"],
|
|
365
|
+
response: {
|
|
366
|
+
200: {
|
|
367
|
+
type: "array",
|
|
368
|
+
items: {
|
|
369
|
+
type: "object",
|
|
370
|
+
properties: {
|
|
371
|
+
id: { type: "string" },
|
|
372
|
+
email: { type: "string" },
|
|
373
|
+
name: { type: "string" },
|
|
374
|
+
role: { type: "string" },
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
async () => {
|
|
382
|
+
const users = await fastify.prisma.user.findMany({
|
|
383
|
+
select: { id: true, email: true, name: true, role: true },
|
|
384
|
+
});
|
|
385
|
+
return users;
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// GET /users/:id
|
|
390
|
+
fastify.get<{ Params: { id: string } }>(
|
|
391
|
+
"/:id",
|
|
392
|
+
{
|
|
393
|
+
schema: {
|
|
394
|
+
description: "Get user by ID",
|
|
395
|
+
tags: ["Users"],
|
|
396
|
+
params: {
|
|
397
|
+
type: "object",
|
|
398
|
+
properties: {
|
|
399
|
+
id: { type: "string" },
|
|
400
|
+
},
|
|
401
|
+
required: ["id"],
|
|
402
|
+
},
|
|
403
|
+
response: {
|
|
404
|
+
200: {
|
|
405
|
+
type: "object",
|
|
406
|
+
properties: {
|
|
407
|
+
id: { type: "string" },
|
|
408
|
+
email: { type: "string" },
|
|
409
|
+
name: { type: "string" },
|
|
410
|
+
role: { type: "string" },
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
404: {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: {
|
|
416
|
+
error: { type: "string" },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
async (request, reply) => {
|
|
423
|
+
const { id } = request.params;
|
|
424
|
+
const user = await fastify.prisma.user.findUnique({
|
|
425
|
+
where: { id },
|
|
426
|
+
select: { id: true, email: true, name: true, role: true },
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (!user) {
|
|
430
|
+
return reply.code(404).send({ error: "User not found" });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return user;
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// POST /users
|
|
438
|
+
fastify.post<{ Body: { email: string; name?: string; password: string } }>(
|
|
439
|
+
"/",
|
|
440
|
+
{
|
|
441
|
+
schema: {
|
|
442
|
+
description: "Create a new user",
|
|
443
|
+
tags: ["Users"],
|
|
444
|
+
body: {
|
|
445
|
+
type: "object",
|
|
446
|
+
properties: {
|
|
447
|
+
email: { type: "string", format: "email" },
|
|
448
|
+
name: { type: "string" },
|
|
449
|
+
password: { type: "string", minLength: 8 },
|
|
450
|
+
},
|
|
451
|
+
required: ["email", "password"],
|
|
452
|
+
},
|
|
453
|
+
response: {
|
|
454
|
+
201: {
|
|
455
|
+
type: "object",
|
|
456
|
+
properties: {
|
|
457
|
+
id: { type: "string" },
|
|
458
|
+
email: { type: "string" },
|
|
459
|
+
name: { type: "string" },
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
async (request, reply) => {
|
|
466
|
+
const input = createUserSchema.parse(request.body);
|
|
467
|
+
const hashedPassword = await bcrypt.hash(input.password, 10);
|
|
468
|
+
|
|
469
|
+
const user = await fastify.prisma.user.create({
|
|
470
|
+
data: {
|
|
471
|
+
...input,
|
|
472
|
+
password: hashedPassword,
|
|
473
|
+
},
|
|
474
|
+
select: { id: true, email: true, name: true },
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return reply.code(201).send(user);
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// PATCH /users/:id
|
|
482
|
+
fastify.patch<{ Params: { id: string }; Body: { name?: string; email?: string } }>(
|
|
483
|
+
"/:id",
|
|
484
|
+
{
|
|
485
|
+
schema: {
|
|
486
|
+
description: "Update user",
|
|
487
|
+
tags: ["Users"],
|
|
488
|
+
params: {
|
|
489
|
+
type: "object",
|
|
490
|
+
properties: {
|
|
491
|
+
id: { type: "string" },
|
|
492
|
+
},
|
|
493
|
+
required: ["id"],
|
|
494
|
+
},
|
|
495
|
+
body: {
|
|
496
|
+
type: "object",
|
|
497
|
+
properties: {
|
|
498
|
+
name: { type: "string" },
|
|
499
|
+
email: { type: "string", format: "email" },
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
async (request, reply) => {
|
|
505
|
+
const { id } = request.params;
|
|
506
|
+
const input = updateUserSchema.parse(request.body);
|
|
507
|
+
|
|
508
|
+
const user = await fastify.prisma.user.update({
|
|
509
|
+
where: { id },
|
|
510
|
+
data: input,
|
|
511
|
+
select: { id: true, email: true, name: true },
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
return user;
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// DELETE /users/:id
|
|
519
|
+
fastify.delete<{ Params: { id: string } }>(
|
|
520
|
+
"/:id",
|
|
521
|
+
{
|
|
522
|
+
schema: {
|
|
523
|
+
description: "Delete user",
|
|
524
|
+
tags: ["Users"],
|
|
525
|
+
params: {
|
|
526
|
+
type: "object",
|
|
527
|
+
properties: {
|
|
528
|
+
id: { type: "string" },
|
|
529
|
+
},
|
|
530
|
+
required: ["id"],
|
|
531
|
+
},
|
|
532
|
+
response: {
|
|
533
|
+
204: { type: "null" },
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
async (request, reply) => {
|
|
538
|
+
const { id } = request.params;
|
|
539
|
+
await fastify.prisma.user.delete({ where: { id } });
|
|
540
|
+
return reply.code(204).send();
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
};
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Paso 11: Crear app builder
|
|
547
|
+
|
|
548
|
+
Crear `src/app.ts`:
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
import Fastify, { FastifyInstance } from "fastify";
|
|
552
|
+
import cors from "@fastify/cors";
|
|
553
|
+
import helmet from "@fastify/helmet";
|
|
554
|
+
import rateLimit from "@fastify/rate-limit";
|
|
555
|
+
import prismaPlugin from "./plugins/prisma";
|
|
556
|
+
import swaggerPlugin from "./plugins/swagger";
|
|
557
|
+
import { healthRoutes } from "./routes/health";
|
|
558
|
+
import { userRoutes } from "./routes/users";
|
|
559
|
+
import { env } from "./config/env";
|
|
560
|
+
|
|
561
|
+
export async function buildApp(): Promise<FastifyInstance> {
|
|
562
|
+
const app = Fastify({
|
|
563
|
+
logger: {
|
|
564
|
+
level: env.NODE_ENV === "production" ? "info" : "debug",
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// Plugins de seguridad
|
|
569
|
+
await app.register(cors, {
|
|
570
|
+
origin: env.NODE_ENV === "production" ? "https://yourapp.com" : true,
|
|
571
|
+
});
|
|
572
|
+
await app.register(helmet);
|
|
573
|
+
await app.register(rateLimit, {
|
|
574
|
+
max: env.RATE_LIMIT_MAX,
|
|
575
|
+
timeWindow: "1 minute",
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Database
|
|
579
|
+
await app.register(prismaPlugin);
|
|
580
|
+
|
|
581
|
+
// Documentation
|
|
582
|
+
await app.register(swaggerPlugin);
|
|
583
|
+
|
|
584
|
+
// Routes
|
|
585
|
+
await app.register(healthRoutes);
|
|
586
|
+
await app.register(userRoutes, { prefix: "/users" });
|
|
587
|
+
|
|
588
|
+
return app;
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Paso 12: Crear entry point
|
|
593
|
+
|
|
594
|
+
Crear `src/index.ts`:
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
import { buildApp } from "./app";
|
|
598
|
+
import { env } from "./config/env";
|
|
599
|
+
|
|
600
|
+
async function main() {
|
|
601
|
+
const app = await buildApp();
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
await app.listen({ port: env.PORT, host: env.HOST });
|
|
605
|
+
console.log(`Server running at http://${env.HOST}:${env.PORT}`);
|
|
606
|
+
console.log(`Docs at http://${env.HOST}:${env.PORT}/docs`);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
app.log.error(err);
|
|
609
|
+
process.exit(1);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
main();
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Paso 13: Configurar Docker
|
|
617
|
+
|
|
618
|
+
Crear `Dockerfile`:
|
|
619
|
+
|
|
620
|
+
```dockerfile
|
|
621
|
+
FROM node:20-alpine AS base
|
|
622
|
+
WORKDIR /app
|
|
623
|
+
|
|
624
|
+
# Dependencies
|
|
625
|
+
FROM base AS deps
|
|
626
|
+
COPY package*.json ./
|
|
627
|
+
RUN npm ci --only=production
|
|
628
|
+
|
|
629
|
+
# Build
|
|
630
|
+
FROM base AS build
|
|
631
|
+
COPY package*.json ./
|
|
632
|
+
RUN npm ci
|
|
633
|
+
COPY . .
|
|
634
|
+
RUN npx prisma generate
|
|
635
|
+
RUN npm run build
|
|
636
|
+
|
|
637
|
+
# Production
|
|
638
|
+
FROM base AS production
|
|
639
|
+
ENV NODE_ENV=production
|
|
640
|
+
|
|
641
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
642
|
+
COPY --from=build /app/dist ./dist
|
|
643
|
+
COPY --from=build /app/node_modules/.prisma ./node_modules/.prisma
|
|
644
|
+
COPY prisma ./prisma
|
|
645
|
+
|
|
646
|
+
EXPOSE 3000
|
|
647
|
+
|
|
648
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
649
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
|
650
|
+
|
|
651
|
+
CMD ["node", "dist/index.js"]
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Crear `docker-compose.yml`:
|
|
655
|
+
|
|
656
|
+
```yaml
|
|
657
|
+
version: "3.8"
|
|
658
|
+
|
|
659
|
+
services:
|
|
660
|
+
api:
|
|
661
|
+
build: .
|
|
662
|
+
ports:
|
|
663
|
+
- "3000:3000"
|
|
664
|
+
environment:
|
|
665
|
+
- NODE_ENV=production
|
|
666
|
+
- DATABASE_URL=postgresql://postgres:password@db:5432/myapi
|
|
667
|
+
- JWT_SECRET=your-secret-key-at-least-32-characters
|
|
668
|
+
depends_on:
|
|
669
|
+
db:
|
|
670
|
+
condition: service_healthy
|
|
671
|
+
|
|
672
|
+
db:
|
|
673
|
+
image: postgres:16-alpine
|
|
674
|
+
environment:
|
|
675
|
+
- POSTGRES_USER=postgres
|
|
676
|
+
- POSTGRES_PASSWORD=password
|
|
677
|
+
- POSTGRES_DB=myapi
|
|
678
|
+
volumes:
|
|
679
|
+
- postgres_data:/var/lib/postgresql/data
|
|
680
|
+
healthcheck:
|
|
681
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
682
|
+
interval: 5s
|
|
683
|
+
timeout: 5s
|
|
684
|
+
retries: 5
|
|
685
|
+
|
|
686
|
+
volumes:
|
|
687
|
+
postgres_data:
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Paso 14: Scripts de package.json
|
|
691
|
+
|
|
692
|
+
```json
|
|
693
|
+
{
|
|
694
|
+
"scripts": {
|
|
695
|
+
"dev": "tsx watch src/index.ts",
|
|
696
|
+
"build": "tsc",
|
|
697
|
+
"start": "node dist/index.js",
|
|
698
|
+
"lint": "eslint src --ext .ts",
|
|
699
|
+
"test": "vitest",
|
|
700
|
+
"db:migrate": "prisma migrate dev",
|
|
701
|
+
"db:push": "prisma db push",
|
|
702
|
+
"db:studio": "prisma studio",
|
|
703
|
+
"docker:build": "docker build -t my-api .",
|
|
704
|
+
"docker:run": "docker-compose up -d"
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
</setup_steps>
|
|
709
|
+
|
|
710
|
+
<verification>
|
|
711
|
+
## Verificacion
|
|
712
|
+
|
|
713
|
+
### 1. Ejecutar migraciones
|
|
714
|
+
```bash
|
|
715
|
+
npx prisma migrate dev --name init
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### 2. Iniciar servidor
|
|
719
|
+
```bash
|
|
720
|
+
npm run dev
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### 3. Verificar health check
|
|
724
|
+
```bash
|
|
725
|
+
curl http://localhost:3000/health
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### 4. Ver documentacion
|
|
729
|
+
Abrir http://localhost:3000/docs
|
|
730
|
+
|
|
731
|
+
### 5. Probar CRUD
|
|
732
|
+
```bash
|
|
733
|
+
# Crear usuario
|
|
734
|
+
curl -X POST http://localhost:3000/users \
|
|
735
|
+
-H "Content-Type: application/json" \
|
|
736
|
+
-d '{"email":"test@example.com","password":"password123"}'
|
|
737
|
+
|
|
738
|
+
# Listar usuarios
|
|
739
|
+
curl http://localhost:3000/users
|
|
740
|
+
|
|
741
|
+
# Obtener usuario
|
|
742
|
+
curl http://localhost:3000/users/{id}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### 6. Probar Docker
|
|
746
|
+
```bash
|
|
747
|
+
docker-compose up -d
|
|
748
|
+
curl http://localhost:3000/health
|
|
749
|
+
```
|
|
750
|
+
</verification>
|
|
751
|
+
|
|
752
|
+
<common_issues>
|
|
753
|
+
## Problemas Comunes
|
|
754
|
+
|
|
755
|
+
### "Cannot find module"
|
|
756
|
+
- Verificar tsconfig paths
|
|
757
|
+
- Ejecutar `npx prisma generate`
|
|
758
|
+
|
|
759
|
+
### "Database connection failed"
|
|
760
|
+
- Verificar DATABASE_URL
|
|
761
|
+
- Verificar que PostgreSQL esta corriendo
|
|
762
|
+
|
|
763
|
+
### "Rate limit exceeded"
|
|
764
|
+
- Ajustar RATE_LIMIT_MAX en .env
|
|
765
|
+
</common_issues>
|