| name | dev-express-guide |
| description | Développement d'APIs Node.js avec Express, middleware, routing, gestion d'erreurs, authentification et bonnes pratiques de conception. Se déclenche avec "Express", "Express.js", "middleware Express", "Node.js API", "router Express". Also triggers on "Express API", "Node.js middleware", "Express error handling". |
Guide Express.js
Workflow
1. Initialiser le projet
mkdir my-api && cd my-api
npm init -y
npm install express helmet cors morgan compression express-rate-limit
npm install zod jsonwebtoken bcryptjs
npm install -D typescript ts-node @types/express @types/node nodemon jest supertest
Structure recommandée (feature-based) :
src/
app.ts ← config Express, middleware globaux
server.ts ← écoute HTTP + graceful shutdown
routes/ ← index + un fichier par ressource
controllers/ ← extraction req/res, appel service
services/ ← logique métier (testable)
middleware/ ← auth, validate, errorHandler
models/ ← schémas DB (Prisma / Mongoose)
utils/ ← helpers, logger, asyncHandler
types/ ← interfaces TS globales
2. Configurer app.ts avec les middleware globaux
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import morgan from 'morgan';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
import { errorHandler } from './middleware/errorHandler';
import { router } from './routes';
const app = express();
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
app.use(express.json({ limit: '1mb' }));
app.use(morgan('combined'));
app.use(compression());
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use('/api/v1', router);
app.use(errorHandler);
export { app };
3. Router modulaire par ressource
import { Router } from 'express';
import { authenticate } from '../middleware/auth';
import { validate } from '../middleware/validate';
import { createUserSchema } from '../schemas/user.schema';
import { createUser, getUserById } from '../controllers/user.controller';
const router = Router();
router.post('/', validate(createUserSchema), createUser);
router.get('/:id', authenticate, getUserById);
export { router as userRouter };
import { Router } from 'express';
import { userRouter } from './users';
const router = Router();
router.use('/users', userRouter);
export { router };
4. Pattern controller → service
import { RequestHandler } from 'express';
export const asyncHandler =
(fn: RequestHandler): RequestHandler =>
(req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
import { asyncHandler } from '../middleware/asyncHandler';
import { UserService } from '../services/user.service';
export const createUser = asyncHandler(async (req, res) => {
const user = await UserService.create(req.body);
res.status(201).json({ success: true, data: user });
});
import { prisma } from '../utils/db';
import bcrypt from 'bcryptjs';
export const UserService = {
async create(data: CreateUserDto) {
const hashed = await bcrypt.hash(data.password, 12);
return prisma.user.create({ data: { ...data, password: hashed } });
},
};
5. Validation avec Zod
import { AnyZodObject } from 'zod';
import { RequestHandler } from 'express';
export const validate =
(schema: AnyZodObject): RequestHandler =>
(req, _res, next) => {
const result = schema.safeParse({
body: req.body,
params: req.params,
query: req.query,
});
if (!result.success) {
return next({ status: 422, errors: result.error.flatten() });
}
req.body = result.data.body;
next();
};
import { z } from 'zod';
export const createUserSchema = z.object({
body: z.object({
email: z.string().email(),
password: z.string().(),
: z.().().(),
}),
});
6. Gestion d'erreurs centralisée
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number,
public errors?: unknown
) {
super(message);
}
}
import { ErrorRequestHandler } from 'express';
import { AppError } from '../utils/AppError';
export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
const status = err instanceof AppError ? err.statusCode : err.status ?? 500;
res.status(status).json({
success: false,
message: err.message ?? 'Internal Server Error',
errors: err.errors ?? undefined,
...(process.env.NODE_ENV === 'development' && { : err. }),
});
};
7. Authentification JWT
import jwt from 'jsonwebtoken';
import { RequestHandler } from 'express';
import { AppError } from '../utils/AppError';
export const authenticate: RequestHandler = (req, _res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) throw new AppError('Unauthorized', 401);
try {
req.user = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
next();
} catch {
next(new AppError('Token invalide ou expiré', 401));
}
};
8. Graceful shutdown dans server.ts
import { app } from './app';
import { prisma } from './utils/db';
const server = app.listen(process.env.PORT ?? 3000, () =>
console.log(`API up on :${process.env.PORT ?? 3000}`)
);
const shutdown = async (signal: string) => {
console.log(`${signal} received — shutting down`);
server.close(async () => {
await prisma.$disconnect();
process.exit(0);
});
setTimeout(() => process.exit(1), 10_000);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
9. Tests avec Jest + Supertest
import request from 'supertest';
import { app } from '../src/app';
describe('POST /api/v1/users', () => {
it('crée un utilisateur valide', async () => {
const res = await request(app)
.post('/api/v1/users')
.send({ email: 'test@test.com', password: 'secret123', name: 'Alice' });
expect(res.status).toBe(201);
expect(res.body.success).toBe(true);
});
it('rejette un email invalide', async () => {
const res = await request(app)
.post('/api/v1/users')
.send({ email: 'pas-un-email', password: 'secret123', name: 'Alice' });
expect(res.status).();
});
});
Critères de décision
| Besoin | Choix |
|---|
| API REST simple | Express + Zod + Prisma |
| Temps réel | Express + Socket.io ou passer à Fastify |
| Auth OAuth2/OIDC | passport.js + stratégies |
| Validation complexe | Zod (TS-first) > Joi |
| ORM | Prisma (TypeScript), Mongoose (MongoDB) |
| Logging structuré | pino + pino-http (plus rapide que morgan) |
| Haute performance | Envisager Fastify (3× plus rapide) |
Anti-patterns / pièges
- try-catch dans chaque route — utilise
asyncHandler, sinon une exception non catchée crash le process.
- Logique métier dans les routes — non testable, non réutilisable ; toujours isoler dans un service.
app.use(express.json()) après les routes — les routes ne verront pas le body parsé. L'ordre des middleware EST la logique.
- Oublier
next(err) dans les middleware async — une Promise rejetée sans .catch(next) ou asyncHandler ne déclenche pas l'error handler.
res.send() après res.json() — double send → crash. Toujours return res.json(...).
- Variables d'env en dur — utiliser
dotenv + valider avec envalid ou Zod au démarrage.
- Pas de rate limiting — brute force triviale sans
express-rate-limit ou un WAF upstream.
- Port binding dans
app.ts — rend les tests Supertest lents (port déjà occupé) ; séparer app.ts et server.ts.
- Helmet désactivé en prod — headers par défaut exposent la stack tech.
Bonnes pratiques 2026
- TypeScript strict (
"strict": true) — évite les bugs de runtime sur req.params / req.query (toujours string).
- Versioning d'API — préfixer
/api/v1/ dès le début, impossible à ajouter proprement après coup.
- Format de réponse uniforme —
{ success, data, error, message, meta } sur toutes les routes, y compris erreurs.
- Health check —
GET /health renvoie { status: "ok", uptime, db: "connected" } pour les load balancers / k8s.
- OpenAPI — générer la spec avec
zod-to-openapi ou tsoa pour documenter et valider les contrats.
pino à la place de console.log — JSON structuré, niveaux, corrélation par requestId.
Communication Rules — MANDATORY
- Ultra-concise. No filler, no preamble, no pleasantries.
- Never say "happy to help", "sure!", "great question", "let me", or similar.
- Tool first, talk second. Act before explaining.
- Result first. Lead with outcome, not process.
- Stop when done. No summary, no recap, no trailing commentary.
- No politeness wrappers. Direct and blunt.
- Minimum words. If one word works, do not use ten.
- No unsolicited explanations.
- No emoji unless asked.