@eltonssouza/development-utility-kit 0.10.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/.claude/agents/README.md +24 -0
- package/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +46 -0
- package/.claude/agents/stack-resolver.md +104 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/hooks/flow-guard.js +261 -0
- package/.claude/hooks/flow-state.js +197 -0
- package/.claude/local/CLAUDE.md +71 -0
- package/.claude/settings.json +55 -0
- package/.claude/skills/README.md +331 -0
- package/.claude/skills/active-project/SKILL.md +131 -0
- package/.claude/skills/api-integration-test/SKILL.md +84 -0
- package/.claude/skills/auto-test-guard/SKILL.md +239 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +62 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +189 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +80 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +86 -0
- package/.claude/skills/project-manager/SKILL.md +334 -0
- package/.claude/skills/quality-standards/SKILL.md +203 -0
- package/.claude/skills/quick-feature/SKILL.md +266 -0
- package/.claude/skills/run-sprint/SKILL.md +41 -0
- package/.claude/skills/scaffold/SKILL.md +60 -0
- package/.claude/skills/stack-discovery/SKILL.md +161 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +87 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +256 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +97 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/dotnet/aspire-9.md +528 -0
- package/.claude/stacks/go/gin-1.10.md +570 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/node/express-5.md +538 -0
- package/.claude/stacks/python/django-5.md +483 -0
- package/.claude/stacks/python/fastapi-0.115.md +522 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +472 -0
- package/README.md +412 -0
- package/bin/cli.js +848 -0
- package/bin/lib/adr.js +146 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/doctor.js +527 -0
- package/bin/lib/help.js +328 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/lint-allowlist.json +15 -0
- package/bin/lib/lint.js +798 -0
- package/bin/lib/local-dir.js +68 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +321 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +853 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +252 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/cli-reference.en.md +538 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +404 -0
- package/dashboard/public/content/docs/pipeline.en.md +414 -0
- package/dashboard/public/content/docs/plugins.en.md +289 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +484 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +565 -0
- package/dashboard/public/content/manifest.json +114 -0
- package/dashboard/public/content/manual/backend.en.md +1053 -0
- package/dashboard/public/content/manual/existing-project.en.md +848 -0
- package/dashboard/public/content/manual/frontend.en.md +1008 -0
- package/dashboard/public/content/manual/fullstack.en.md +1459 -0
- package/dashboard/public/content/manual/mobile.en.md +837 -0
- package/dashboard/public/content/manual/quickstart.en.md +169 -0
- package/dashboard/public/index.html +217 -0
- package/dashboard/public/style.css +857 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +421 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
---
|
|
2
|
+
stack: node/express-5
|
|
3
|
+
versions_covered: "5.0.x — 5.2.x"
|
|
4
|
+
last_validated: 2026-05-28
|
|
5
|
+
validated_against: "reference pack — Node 22 LTS + Express 5.0 + Prisma 5.20 + Jest 29"
|
|
6
|
+
status: active
|
|
7
|
+
pack_owner: "@elton"
|
|
8
|
+
security_review: 2026-05-28
|
|
9
|
+
next_review_due: 2027-05-28
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Node 22+ + Express 5.x
|
|
13
|
+
|
|
14
|
+
Canonical knowledge pack for Node.js HTTP services on Express 5.x. Express 5 (released October 2024, after years of beta) brings async error handling as a first-class behavior, Node 18+ floor, modern route matching, and various breaking changes from Express 4. For new projects that want a heavier opinion (DI, validation, OpenAPI), consider NestJS instead — this pack assumes plain Express with TypeScript and explicit DDD layering.
|
|
15
|
+
|
|
16
|
+
## 1. When to use this pack
|
|
17
|
+
|
|
18
|
+
- Project declares `Primary stack: Node 22+ + Express 5.x + TypeScript` in `## Project Identity`.
|
|
19
|
+
- `package.json` declares `"express": "^5.0.0"` and `"node": ">=22"`.
|
|
20
|
+
- Service is API-first (REST), small-to-medium throughput, team prefers explicit code over framework magic.
|
|
21
|
+
- For SSR with React/Vue or full framework with DI: Next.js, NestJS, or Fastify+plugins are better fits.
|
|
22
|
+
- For high-throughput edge: Hono / Fastify outperform Express. Use this pack if Express's middleware ecosystem is the deciding factor.
|
|
23
|
+
|
|
24
|
+
## 2. Stack baseline (what this pack assumes)
|
|
25
|
+
|
|
26
|
+
| Component | Version range | Notes |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Node.js | 22 LTS (min) / 24 (latest) | Native `--watch`, native `node:test`, native `fetch` — use them |
|
|
29
|
+
| TypeScript | 5.6.x+ | strict mode mandatory; `noUncheckedIndexedAccess: true` |
|
|
30
|
+
| Express | 5.0.x — 5.2.x | Async route handlers handle errors automatically; major param matching changes from 4.x |
|
|
31
|
+
| ORM | Prisma 5.20+ (recommended) OR TypeORM 0.3.20+ | Prisma's typed client + migrations is the cleanest single-tool option |
|
|
32
|
+
| Validation | Zod 3.23+ | TypeScript-first; infer types from schema; use everywhere |
|
|
33
|
+
| Build | `tsc` for compilation OR `tsx` for dev OR esbuild for prod bundles | `tsx watch` for dev; `tsc --noEmit && esbuild` for prod |
|
|
34
|
+
| Package manager | pnpm 9.x (recommended) OR npm 10.x | pnpm is faster + safer (strict deps) |
|
|
35
|
+
| Tests | Jest 29.x OR Vitest 2.x + Supertest 7.x + Testcontainers Node 10.x | NEVER SQLite if prod is Postgres |
|
|
36
|
+
| Mutation | Stryker 8.x | Target ≥70% on `domain/` + `application/` |
|
|
37
|
+
| Coverage | Jest `--coverage` (V8 provider) | Target ≥85% lines, ≥80% branches |
|
|
38
|
+
| Static analysis | `eslint` v9 (flat config) + `@typescript-eslint` + `prettier` | `--max-warnings 0` |
|
|
39
|
+
| Security scan | `npm audit --omit=dev` + `snyk test` (optional) | 0 CVE with CVSS ≥7.0 |
|
|
40
|
+
| Observability | OpenTelemetry SDK + `@opentelemetry/instrumentation-express` | W3C Trace Context; auto-instruments routes + Prisma + http |
|
|
41
|
+
| Logger | `pino` 9.x | High performance JSON; structured by default |
|
|
42
|
+
| Config | `zod` for env validation at startup | Crash fast on bad env |
|
|
43
|
+
| Process manager | `pm2` or container runtime | Cluster mode only when needed; Node 22 handles concurrency well single-process |
|
|
44
|
+
|
|
45
|
+
## 3. Project structure (DDD-flavored Express)
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
service/
|
|
49
|
+
├── package.json
|
|
50
|
+
├── pnpm-lock.yaml
|
|
51
|
+
├── tsconfig.json
|
|
52
|
+
├── eslint.config.js # flat config
|
|
53
|
+
├── prisma/
|
|
54
|
+
│ ├── schema.prisma
|
|
55
|
+
│ └── migrations/
|
|
56
|
+
├── .env.example
|
|
57
|
+
├── src/
|
|
58
|
+
│ ├── domain/ # pure TS; no Express, no Prisma
|
|
59
|
+
│ │ ├── product/
|
|
60
|
+
│ │ │ ├── entity.ts
|
|
61
|
+
│ │ │ ├── repository.ts # interface
|
|
62
|
+
│ │ │ └── service.ts
|
|
63
|
+
│ │ └── shared/
|
|
64
|
+
│ ├── application/ # use cases (1 class per use case)
|
|
65
|
+
│ │ ├── product/
|
|
66
|
+
│ │ │ ├── createProduct.ts
|
|
67
|
+
│ │ │ ├── listProducts.ts
|
|
68
|
+
│ │ │ └── dto.ts
|
|
69
|
+
│ │ └── shared/
|
|
70
|
+
│ ├── infrastructure/ # Prisma, fetch wrappers
|
|
71
|
+
│ │ ├── db/
|
|
72
|
+
│ │ │ └── prisma.ts # PrismaClient singleton
|
|
73
|
+
│ │ ├── product/
|
|
74
|
+
│ │ │ └── repository.ts # adapter for domain interface
|
|
75
|
+
│ │ └── http/
|
|
76
|
+
│ │ └── upstreamClient.ts
|
|
77
|
+
│ ├── api/ # Express routes + middleware
|
|
78
|
+
│ │ ├── product/
|
|
79
|
+
│ │ │ ├── router.ts
|
|
80
|
+
│ │ │ ├── handlers.ts
|
|
81
|
+
│ │ │ └── schemas.ts # Zod schemas (req/res)
|
|
82
|
+
│ │ └── shared/
|
|
83
|
+
│ │ ├── errorHandler.ts # RFC 9457 ProblemDetails
|
|
84
|
+
│ │ └── middleware.ts
|
|
85
|
+
│ ├── config/
|
|
86
|
+
│ │ └── env.ts # Zod-validated env
|
|
87
|
+
│ └── server.ts # Express app + listen
|
|
88
|
+
└── tests/
|
|
89
|
+
├── unit/
|
|
90
|
+
├── integration/
|
|
91
|
+
└── e2e/
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Rule**: `domain/` and `application/` contain **zero Express and zero Prisma imports**. They are pure TS testable without HTTP or DB. `infrastructure/` is the only layer importing `@prisma/client`. `api/` is the only layer importing `express`.
|
|
95
|
+
|
|
96
|
+
## 4. Code patterns
|
|
97
|
+
|
|
98
|
+
### Domain entity (no Prisma, no Express)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// src/domain/product/entity.ts
|
|
102
|
+
export class Product {
|
|
103
|
+
constructor(
|
|
104
|
+
public readonly id: string, // UUID
|
|
105
|
+
public readonly name: string,
|
|
106
|
+
public readonly priceCents: bigint, // bigint for money — never number
|
|
107
|
+
public readonly stock: number,
|
|
108
|
+
public readonly createdAt: Date,
|
|
109
|
+
) {}
|
|
110
|
+
|
|
111
|
+
reserve(qty: number): Product {
|
|
112
|
+
if (qty <= 0) throw new Error("qty must be positive");
|
|
113
|
+
if (qty > this.stock) throw new Error("insufficient stock");
|
|
114
|
+
return new Product(this.id, this.name, this.priceCents, this.stock - qty, this.createdAt);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Rule**: money in `bigint` (or `Decimal` from prisma) — never `number` (IEEE 754 loses precision). Domain methods take and return value types. Immutability by constructor + `readonly`.
|
|
120
|
+
|
|
121
|
+
### Domain port + infrastructure adapter (Prisma)
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// src/domain/product/repository.ts
|
|
125
|
+
import type { Product } from "./entity";
|
|
126
|
+
|
|
127
|
+
export interface ProductRepository {
|
|
128
|
+
get(id: string): Promise<Product | null>;
|
|
129
|
+
save(product: Product): Promise<void>;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// src/infrastructure/product/repository.ts
|
|
135
|
+
import type { PrismaClient, Product as ProductRow } from "@prisma/client";
|
|
136
|
+
import type { ProductRepository } from "@/domain/product/repository";
|
|
137
|
+
import { Product } from "@/domain/product/entity";
|
|
138
|
+
|
|
139
|
+
export class PrismaProductRepository implements ProductRepository {
|
|
140
|
+
constructor(private readonly prisma: PrismaClient) {}
|
|
141
|
+
|
|
142
|
+
async get(id: string): Promise<Product | null> {
|
|
143
|
+
const row = await this.prisma.product.findUnique({ where: { id } });
|
|
144
|
+
return row ? this.toDomain(row) : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async save(p: Product): Promise<void> {
|
|
148
|
+
await this.prisma.product.upsert({
|
|
149
|
+
where: { id: p.id },
|
|
150
|
+
create: { id: p.id, name: p.name, priceCents: p.priceCents, stock: p.stock },
|
|
151
|
+
update: { name: p.name, priceCents: p.priceCents, stock: p.stock },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private toDomain(row: ProductRow): Product {
|
|
156
|
+
return new Product(row.id, row.name, row.priceCents, row.stock, row.createdAt);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Rule**: domain `ProductRepository` is an interface in `domain/`. Implementation imports Prisma. Use cases depend on the interface. UUID primary key.
|
|
162
|
+
|
|
163
|
+
### Zod schemas + handler (Express 5 async routes)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// src/api/product/schemas.ts
|
|
167
|
+
import { z } from "zod";
|
|
168
|
+
|
|
169
|
+
export const createProductRequest = z.object({
|
|
170
|
+
name: z.string().min(1).max(120),
|
|
171
|
+
priceCents: z.coerce.bigint().positive(),
|
|
172
|
+
stock: z.number().int().min(0),
|
|
173
|
+
}).strict(); // strict() rejects unknown keys
|
|
174
|
+
|
|
175
|
+
export type CreateProductRequest = z.infer<typeof createProductRequest>;
|
|
176
|
+
|
|
177
|
+
export const productResponse = z.object({
|
|
178
|
+
id: z.string().uuid(),
|
|
179
|
+
name: z.string(),
|
|
180
|
+
priceCents: z.bigint(),
|
|
181
|
+
stock: z.number(),
|
|
182
|
+
createdAt: z.date(),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
export type ProductResponse = z.infer<typeof productResponse>;
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// src/api/product/handlers.ts
|
|
190
|
+
import type { Request, Response } from "express";
|
|
191
|
+
import { createProductRequest } from "./schemas";
|
|
192
|
+
import type { CreateProductUseCase } from "@/application/product/createProduct";
|
|
193
|
+
|
|
194
|
+
export class ProductHandlers {
|
|
195
|
+
constructor(private readonly createUC: CreateProductUseCase) {}
|
|
196
|
+
|
|
197
|
+
// Express 5: async errors propagate to error middleware automatically
|
|
198
|
+
create = async (req: Request, res: Response) => {
|
|
199
|
+
const parsed = createProductRequest.parse(req.body); // throws ZodError on invalid
|
|
200
|
+
const product = await this.createUC.execute(parsed);
|
|
201
|
+
res.status(201).json({
|
|
202
|
+
id: product.id,
|
|
203
|
+
name: product.name,
|
|
204
|
+
priceCents: product.priceCents.toString(),
|
|
205
|
+
stock: product.stock,
|
|
206
|
+
createdAt: product.createdAt,
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Rule**: Zod schemas are the API contract. `parse()` throws on invalid; error middleware catches and returns RFC 9457. `strict()` rejects unknown keys (silent acceptance = footgun).
|
|
213
|
+
|
|
214
|
+
### Router + middleware chain
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// src/api/product/router.ts
|
|
218
|
+
import { Router } from "express";
|
|
219
|
+
import type { ProductHandlers } from "./handlers";
|
|
220
|
+
|
|
221
|
+
export function productRouter(h: ProductHandlers): Router {
|
|
222
|
+
const r = Router();
|
|
223
|
+
r.post("/", h.create);
|
|
224
|
+
return r;
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// src/server.ts
|
|
230
|
+
import express from "express";
|
|
231
|
+
import helmet from "helmet";
|
|
232
|
+
import compression from "compression";
|
|
233
|
+
import cors from "cors";
|
|
234
|
+
import pinoHttp from "pino-http";
|
|
235
|
+
import { productRouter } from "@/api/product/router";
|
|
236
|
+
import { errorHandler } from "@/api/shared/errorHandler";
|
|
237
|
+
import { config } from "@/config/env";
|
|
238
|
+
|
|
239
|
+
const app = express();
|
|
240
|
+
app.disable("x-powered-by");
|
|
241
|
+
app.use(helmet());
|
|
242
|
+
app.use(compression());
|
|
243
|
+
app.use(cors({ origin: config.CORS_ORIGINS, credentials: true }));
|
|
244
|
+
app.use(express.json({ limit: "100kb" }));
|
|
245
|
+
app.use(pinoHttp({ logger }));
|
|
246
|
+
|
|
247
|
+
app.use("/api/v1/products", productRouter(deps.productHandlers));
|
|
248
|
+
|
|
249
|
+
// Centralized error handler — Express 5 catches async errors here
|
|
250
|
+
app.use(errorHandler);
|
|
251
|
+
|
|
252
|
+
app.listen(config.PORT, () => {
|
|
253
|
+
logger.info({ port: config.PORT }, "server up");
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Rule**: `helmet()` before any routes. `express.json({ limit: "100kb" })` always — no default = DoS via large payload. `app.disable("x-powered-by")` — small but free hardening.
|
|
258
|
+
|
|
259
|
+
### Error handler (RFC 9457 ProblemDetails)
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// src/api/shared/errorHandler.ts
|
|
263
|
+
import type { ErrorRequestHandler } from "express";
|
|
264
|
+
import { ZodError } from "zod";
|
|
265
|
+
|
|
266
|
+
export const errorHandler: ErrorRequestHandler = (err, req, res, _next) => {
|
|
267
|
+
if (err instanceof ZodError) {
|
|
268
|
+
res.status(422).json({
|
|
269
|
+
type: "https://example.com/errors/validation",
|
|
270
|
+
title: "Validation failed",
|
|
271
|
+
status: 422,
|
|
272
|
+
instance: req.originalUrl,
|
|
273
|
+
errors: err.errors,
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// log full error server-side
|
|
278
|
+
req.log?.error({ err }, "unhandled error");
|
|
279
|
+
res.status(500).json({
|
|
280
|
+
type: "about:blank",
|
|
281
|
+
title: "Internal Server Error",
|
|
282
|
+
status: 500,
|
|
283
|
+
instance: req.originalUrl,
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Rule**: error handler must be the LAST middleware. Express 5 routes catch async errors and pass to this middleware automatically (huge improvement over Express 4 where every async route needed try/catch or `express-async-errors`).
|
|
289
|
+
|
|
290
|
+
## 5. Testing
|
|
291
|
+
|
|
292
|
+
### Unit (Jest, no Express/DB)
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// src/domain/product/entity.test.ts
|
|
296
|
+
import { Product } from "./entity";
|
|
297
|
+
|
|
298
|
+
describe("Product.reserve", () => {
|
|
299
|
+
const make = (stock: number) =>
|
|
300
|
+
new Product("id-1", "x", 990n, stock, new Date());
|
|
301
|
+
|
|
302
|
+
it("decreases stock", () => {
|
|
303
|
+
expect(make(10).reserve(3).stock).toBe(7);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("refuses zero", () => {
|
|
307
|
+
expect(() => make(10).reserve(0)).toThrow();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("refuses excess", () => {
|
|
311
|
+
expect(() => make(10).reserve(11)).toThrow();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Integration (Supertest + Testcontainers Postgres)
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// tests/integration/product.test.ts
|
|
320
|
+
import request from "supertest";
|
|
321
|
+
import { PostgreSqlContainer } from "@testcontainers/postgresql";
|
|
322
|
+
import { buildApp } from "@/server";
|
|
323
|
+
|
|
324
|
+
describe("POST /api/v1/products", () => {
|
|
325
|
+
let pg: any;
|
|
326
|
+
let app: any;
|
|
327
|
+
|
|
328
|
+
beforeAll(async () => {
|
|
329
|
+
pg = await new PostgreSqlContainer("postgres:16-alpine").start();
|
|
330
|
+
process.env.DATABASE_URL = pg.getConnectionUri();
|
|
331
|
+
app = await buildApp();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
afterAll(async () => {
|
|
335
|
+
await pg.stop();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("creates a product", async () => {
|
|
339
|
+
const r = await request(app)
|
|
340
|
+
.post("/api/v1/products")
|
|
341
|
+
.send({ name: "widget", priceCents: "990", stock: 100 });
|
|
342
|
+
expect(r.status).toBe(201);
|
|
343
|
+
expect(r.body.name).toBe("widget");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Rule**: Testcontainers Postgres for integration tests. NEVER SQLite if prod is Postgres.
|
|
349
|
+
|
|
350
|
+
### Mutation (Stryker)
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
npx stryker run
|
|
354
|
+
# stryker.conf.mjs targets src/domain and src/application
|
|
355
|
+
# Target: mutation score >= 70%
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## 6. Build & run commands
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
# Setup
|
|
362
|
+
pnpm install
|
|
363
|
+
|
|
364
|
+
# Generate Prisma client (after schema change)
|
|
365
|
+
pnpm prisma generate
|
|
366
|
+
|
|
367
|
+
# Migrations
|
|
368
|
+
pnpm prisma migrate dev --name add_products # dev: creates + applies
|
|
369
|
+
pnpm prisma migrate deploy # prod: only applies
|
|
370
|
+
|
|
371
|
+
# Run dev (with auto-reload via tsx)
|
|
372
|
+
pnpm dev # tsx watch src/server.ts
|
|
373
|
+
|
|
374
|
+
# Run prod (after build)
|
|
375
|
+
pnpm build # tsc → dist/
|
|
376
|
+
pnpm start # node dist/server.js
|
|
377
|
+
|
|
378
|
+
# Tests
|
|
379
|
+
pnpm test # jest
|
|
380
|
+
pnpm test:unit # unit only
|
|
381
|
+
pnpm test --coverage # with coverage
|
|
382
|
+
pnpm test:watch
|
|
383
|
+
|
|
384
|
+
# Mutation
|
|
385
|
+
pnpm stryker run
|
|
386
|
+
|
|
387
|
+
# Lint + format
|
|
388
|
+
pnpm lint # eslint
|
|
389
|
+
pnpm format # prettier --write
|
|
390
|
+
pnpm typecheck # tsc --noEmit
|
|
391
|
+
|
|
392
|
+
# Security scan
|
|
393
|
+
pnpm audit --omit=dev
|
|
394
|
+
pnpm dlx snyk test # optional, if Snyk account
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## 7. Security (per ADR-007 + ADR-027 — MANDATORY section)
|
|
398
|
+
|
|
399
|
+
### 7.1 Authentication & Authorization
|
|
400
|
+
|
|
401
|
+
- **JWT**: `jsonwebtoken` 9.x. RS256 preferred multi-service; HS256 single-service. Validate `iss`, `aud`, `exp`. `kid` rotation.
|
|
402
|
+
- **OAuth2 / OIDC**: `openid-client` or hosted IdP (Auth0 / Keycloak / Cognito).
|
|
403
|
+
- **Sessions**: `express-session` + Redis store. Secure cookie flags: `httpOnly`, `secure`, `sameSite: "lax"`.
|
|
404
|
+
- **Password hashing**: `bcrypt` cost 12 minimum OR `argon2` (preferred). NEVER plain `crypto.createHash("sha256")`.
|
|
405
|
+
- **API keys**: `crypto.randomBytes(32).toString("base64url")`; hash before storage.
|
|
406
|
+
- **Authorization**: middleware that decodes JWT, attaches `req.user`, then per-route role check. Object-level checks inside the use case.
|
|
407
|
+
|
|
408
|
+
### 7.2 CORS
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
app.use(cors({
|
|
412
|
+
origin: ["https://app.example.com"], // NEVER "*" in prod
|
|
413
|
+
credentials: true,
|
|
414
|
+
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
415
|
+
allowedHeaders: ["Authorization", "Content-Type"],
|
|
416
|
+
}));
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### 7.3 Validation & input sanitization
|
|
420
|
+
|
|
421
|
+
- **SQL injection**: Prisma parameterizes everything. NEVER `$queryRawUnsafe` with template strings; use `$queryRaw` with the tagged template literal (Prisma parameterizes).
|
|
422
|
+
- **NoSQL injection**: if using MongoDB, never accept raw `$where`/`$regex` from user input.
|
|
423
|
+
- **Zod everywhere**: every route body, query, params validated. `.strict()` to reject unknown keys.
|
|
424
|
+
- **JSON body limit**: `express.json({ limit: "100kb" })` — default is 100kb, but be explicit. Larger uploads should be multipart.
|
|
425
|
+
- **Path traversal**: `path.resolve` + check inside an allowed base before opening any file.
|
|
426
|
+
- **Prototype pollution**: never `Object.assign(target, JSON.parse(userInput))` — Zod schemas prevent this naturally.
|
|
427
|
+
|
|
428
|
+
### 7.4 Secrets management
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// src/config/env.ts
|
|
432
|
+
import { z } from "zod";
|
|
433
|
+
|
|
434
|
+
const envSchema = z.object({
|
|
435
|
+
NODE_ENV: z.enum(["development", "test", "production"]),
|
|
436
|
+
PORT: z.coerce.number().int().positive().default(3000),
|
|
437
|
+
DATABASE_URL: z.string().url(),
|
|
438
|
+
JWT_SECRET: z.string().min(32),
|
|
439
|
+
CORS_ORIGINS: z.string().transform((s) => s.split(",")),
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
export const config = envSchema.parse(process.env); // throws on bad env
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
- `.env` gitignored; `.env.example` committed.
|
|
446
|
+
- Crash on missing/invalid env at startup. Never `process.env.FOO ?? "fallback"` — silent fallback masks misconfiguration.
|
|
447
|
+
- Prefer Secrets Manager (AWS / GCP / Vault) in prod.
|
|
448
|
+
|
|
449
|
+
### 7.5 Rate limiting
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
import rateLimit from "express-rate-limit";
|
|
453
|
+
|
|
454
|
+
const authLimiter = rateLimit({
|
|
455
|
+
windowMs: 60_000,
|
|
456
|
+
max: 5,
|
|
457
|
+
standardHeaders: true,
|
|
458
|
+
legacyHeaders: false,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
app.use("/api/v1/auth", authLimiter);
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
- `5/min` on `/auth/login`, `/auth/signup`, `/auth/password-reset`.
|
|
465
|
+
- `100/min` on regular GETs.
|
|
466
|
+
- Behind reverse proxy: `app.set("trust proxy", 1)` and `keyGenerator: (req) => req.ip` carefully — only trust the proxy IP.
|
|
467
|
+
|
|
468
|
+
### 7.6 OWASP Top 10 mapping
|
|
469
|
+
|
|
470
|
+
| OWASP | Mitigation in Node 22 + Express 5 |
|
|
471
|
+
|---|---|
|
|
472
|
+
| A01 Broken Access Control | JWT middleware + per-route role check; object-level check in use case; default-deny |
|
|
473
|
+
| A02 Cryptographic Failures | bcrypt cost 12 or argon2; TLS at proxy; HSTS via `helmet({ hsts: { maxAge: 31536000 } })`; JWT RS256 with key rotation |
|
|
474
|
+
| A03 Injection | Prisma parameterization; Zod validation on every input; never `$queryRawUnsafe` |
|
|
475
|
+
| A04 Insecure Design | DDD layering enforced (`domain/` no Express/Prisma); use case = single async method per class; ADRs document deviations |
|
|
476
|
+
| A05 Security Misconfiguration | `helmet()`; `app.disable("x-powered-by")`; `express.json({ limit: "100kb" })`; CORS allowlist explicit; `NODE_ENV=production` |
|
|
477
|
+
| A06 Vulnerable Components | `pnpm audit` in CI; `renovate` for `package.json` bumps; lockfile committed |
|
|
478
|
+
| A07 Auth Failures | rate-limit on auth endpoints; argon2/bcrypt; account lock via Redis counter; MFA via `otplib` |
|
|
479
|
+
| A08 Data Integrity | `pnpm install --frozen-lockfile` in CI; SBOM via `cyclonedx-npm`; package-lock signature verification on critical deps |
|
|
480
|
+
| A09 Logging Failures | `pino` structured JSON; correlation ID via `cls-rtracer` or AsyncLocalStorage; **NEVER** log `req.body` raw (PII) |
|
|
481
|
+
| A10 SSRF | Centralized fetch wrapper with allowlist of outbound hosts; reject `localhost`, link-local (`169.254.0.0/16`), private CIDRs by default |
|
|
482
|
+
|
|
483
|
+
### 7.7 LGPD / GDPR / compliance specifics
|
|
484
|
+
|
|
485
|
+
- **PII tagging**: convention via Prisma comments (`/// @pii`) + a custom lint that checks string fields named Email/CPF/Document have it.
|
|
486
|
+
- **Soft delete**: Prisma middleware that intercepts `delete` and rewrites as `update { deletedAt: new Date() }`. NEVER hard delete user-owned data.
|
|
487
|
+
- **Data subject access (Art 15)**: `GET /api/v1/me/export` returns user data as ZIP.
|
|
488
|
+
- **Erasure (Art 17)**: `DELETE /api/v1/me` redacts PII fields while preserving FKs.
|
|
489
|
+
- **Encryption at rest**: PostgreSQL TDE (cloud-managed) or column-level via `prisma-field-encryption` for sensitive fields.
|
|
490
|
+
|
|
491
|
+
## 8. Anti-patterns (block in code-review)
|
|
492
|
+
|
|
493
|
+
| ❌ Bad | ✅ Good | Why |
|
|
494
|
+
|---|---|---|
|
|
495
|
+
| `app.use(express.json())` without `limit` | `app.use(express.json({ limit: "100kb" }))` | Default works but explicit is safer; large limit = DoS |
|
|
496
|
+
| Async route without error handling in Express 4 style (`(req, res) => { foo().then(...) }`) | Express 5 async route — automatic error forwarding | Express 5 handles it; manual try/catch noise removed |
|
|
497
|
+
| `number` for money | `bigint` (cents) or Prisma `Decimal` | IEEE 754 precision loss is real |
|
|
498
|
+
| `process.env.FOO ?? "fallback"` | Zod-validated env, crash on missing | Silent fallback masks misconfig |
|
|
499
|
+
| Returning Prisma row directly from handler | DTO with explicit serialization | API contract leaks data model |
|
|
500
|
+
| `$queryRawUnsafe(`...${userInput}...`)` | `$queryRaw\`...${userInput}...\`` (tagged template) | Prisma parameterizes only the tagged version |
|
|
501
|
+
| `Object.assign(user, req.body)` | Zod parse + explicit field copy | Prototype pollution + mass assignment |
|
|
502
|
+
| `JSON.stringify(err)` in error response | RFC 9457 ProblemDetails with controlled fields | Leaks stack traces / DB internals to client |
|
|
503
|
+
| `console.log` in prod | `pino` structured JSON logger | console.log loses metadata + slow |
|
|
504
|
+
| `setTimeout(() => doWork(), 0)` instead of `setImmediate` or worker thread | Worker threads or queue for CPU-bound work | Event loop blocking under load |
|
|
505
|
+
| `Buffer.from(input, "base64")` without validation | Validate input is base64 first; catch + 400 | Malformed input crashes or returns garbage |
|
|
506
|
+
| Default `eslint` config without strict TS | `@typescript-eslint` strict + `--max-warnings 0` | Type safety must be enforced |
|
|
507
|
+
|
|
508
|
+
## 9. Migration hints — Express 4 → 5
|
|
509
|
+
|
|
510
|
+
Breaking changes worth flagging when `migrator` agent runs Express 4 → 5:
|
|
511
|
+
|
|
512
|
+
- **Node 18 minimum** (Express 5); recommend Node 22 LTS.
|
|
513
|
+
- **Async error handling**: async route handlers that throw now reach the error middleware automatically. Remove `express-async-errors` package and any manual `next(err)` in catch blocks — they still work but are redundant.
|
|
514
|
+
- **`req.param()` removed**: use `req.params.id` / `req.query.id` / `req.body.id` explicitly.
|
|
515
|
+
- **`res.redirect("back")` removed**: use `res.redirect(req.get("Referrer") || "/")`.
|
|
516
|
+
- **Path matching changed**: regex routes need re-validation. `:param?` optional syntax may behave differently. Test all routes.
|
|
517
|
+
- **`path-to-regexp` v8**: stricter parsing. Routes like `/user/:id(\\d+)` may need adjustment.
|
|
518
|
+
- **`req.query` is now `null` if empty** (vs `{}` in v4). Defensive access: `req.query?.foo`.
|
|
519
|
+
- **Body parsers**: `express.json` and `express.urlencoded` no longer accept charsets via `type` option without explicit configuration.
|
|
520
|
+
- **`res.json` returns `this`**: chainable, but `await res.json(...)` may behave subtly differently if you depended on the old void return.
|
|
521
|
+
|
|
522
|
+
Hand off to `migrator` with: current Express version, route count, list of routes using regex patterns, list of middleware using deprecated APIs.
|
|
523
|
+
|
|
524
|
+
## 10. References
|
|
525
|
+
|
|
526
|
+
- [Express 5 migration guide](https://expressjs.com/en/guide/migrating-5.html)
|
|
527
|
+
- [Express docs](https://expressjs.com/en/api.html)
|
|
528
|
+
- [Prisma docs](https://www.prisma.io/docs)
|
|
529
|
+
- [Zod docs](https://zod.dev/)
|
|
530
|
+
- [pino](https://github.com/pinojs/pino)
|
|
531
|
+
- [helmet docs](https://helmetjs.github.io/)
|
|
532
|
+
- [Testcontainers Node](https://node.testcontainers.org/)
|
|
533
|
+
- [Stryker mutation](https://stryker-mutator.io/docs/stryker-js/introduction/)
|
|
534
|
+
- [OWASP Node.js Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html)
|
|
535
|
+
- ADR-007 (Senior+ gate thresholds — coverage ≥85%, mutation ≥70%)
|
|
536
|
+
- ADR-026 (Generic agents + stack packs architecture)
|
|
537
|
+
- ADR-027 (Pack governance — frontmatter + security mandatory + CODEOWNERS + annual review)
|
|
538
|
+
- ADR-029 (Canonical pack format — this document follows it)
|