From 2ac1a3281b049073f88365abda6cd06f5d3dcf64 Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Mon, 15 Jun 2026 17:46:39 +0200 Subject: [PATCH] feat(backend): add global error handling and request logging middlewares --- eme-backend/src/app.ts | 27 ++++++++++++++----- eme-backend/src/errors/app-error.ts | 10 +++++++ eme-backend/src/middlewares/error-handler.ts | 25 +++++++++++++++++ eme-backend/src/middlewares/not-found.ts | 6 +++++ eme-backend/src/middlewares/request-logger.ts | 12 +++++++++ eme-backend/src/utils/logger.ts | 18 +++++++++++++ review.md | 10 ++++++- 7 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 eme-backend/src/errors/app-error.ts create mode 100644 eme-backend/src/middlewares/error-handler.ts create mode 100644 eme-backend/src/middlewares/not-found.ts create mode 100644 eme-backend/src/middlewares/request-logger.ts create mode 100644 eme-backend/src/utils/logger.ts diff --git a/eme-backend/src/app.ts b/eme-backend/src/app.ts index 2a15a70..7d4452c 100644 --- a/eme-backend/src/app.ts +++ b/eme-backend/src/app.ts @@ -2,32 +2,45 @@ import express from 'express'; import cors from 'cors'; import { env } from './config/env'; import { connectDatabase, disconnectDatabase } from './db/prisma'; +import { logger } from './utils/logger'; +import { requestLogger } from './middlewares/request-logger'; +import { notFoundHandler } from './middlewares/not-found'; +import { errorHandler } from './middlewares/error-handler'; const app = express(); -// Middlewares globaux -app.use(cors()); +app.use(cors({ origin: env.frontendUrl })); app.use(express.json()); +app.use(requestLogger); -// Route de test app.get('/health', (_req, res) => { res.json({ status: 'ok', app: 'EME API' }); }); +/* notFoundHandler et errorHandler doivent rester après toutes les routes : + le premier capture les URL non gérées, le second clôt la chaîne Express. */ +app.use(notFoundHandler); +app.use(errorHandler); + async function bootstrap(): Promise { await connectDatabase(); const server = app.listen(env.port, () => { - console.log(`[EME API] Serveur demarre sur http://localhost:${env.port}`); + logger.info(`Serveur demarre sur http://localhost:${env.port}`); }); - process.on('SIGINT', async () => { + const shutdown = async (signal: string): Promise => { + logger.info(`Arret en cours (${signal})`); await disconnectDatabase(); server.close(() => process.exit(0)); - }); + }; + + // SIGTERM est le signal envoyé par Docker à l'arrêt du container. + process.on('SIGINT', () => void shutdown('SIGINT')); + process.on('SIGTERM', () => void shutdown('SIGTERM')); } bootstrap().catch((error: unknown) => { - console.error('[EME API] Echec du demarrage', error); + logger.error('Echec du demarrage', error); process.exit(1); }); diff --git a/eme-backend/src/errors/app-error.ts b/eme-backend/src/errors/app-error.ts new file mode 100644 index 0000000..0ce5cef --- /dev/null +++ b/eme-backend/src/errors/app-error.ts @@ -0,0 +1,10 @@ +export class AppError extends Error { + constructor( + public readonly statusCode: number, + message: string, + ) { + super(message); + // Sans cette ligne, `instanceof AppError` échoue après compilation (cas d'une classe étendant Error). + Object.setPrototypeOf(this, AppError.prototype); + } +} diff --git a/eme-backend/src/middlewares/error-handler.ts b/eme-backend/src/middlewares/error-handler.ts new file mode 100644 index 0000000..dd1bef3 --- /dev/null +++ b/eme-backend/src/middlewares/error-handler.ts @@ -0,0 +1,25 @@ +import { Request, Response, NextFunction } from 'express'; +import { AppError } from '../errors/app-error'; +import { env } from '../config/env'; +import { logger } from '../utils/logger'; + +/* Le 4e argument est requis par Express pour identifier un gestionnaire d'erreurs, + même s'il n'est pas utilisé. */ +export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction): void { + const isAppError = err instanceof AppError; + const statusCode = isAppError ? err.statusCode : 500; + const message = isAppError ? err.message : 'Erreur interne du serveur'; + + if (!isAppError) { + logger.error('Erreur non geree', err); + } + + const exposeStack = env.nodeEnv === 'development' && err instanceof Error && !isAppError; + + res.status(statusCode).json({ + error: { + message, + ...(exposeStack ? { stack: err.stack } : {}), + }, + }); +} diff --git a/eme-backend/src/middlewares/not-found.ts b/eme-backend/src/middlewares/not-found.ts new file mode 100644 index 0000000..923eff9 --- /dev/null +++ b/eme-backend/src/middlewares/not-found.ts @@ -0,0 +1,6 @@ +import { Request, Response, NextFunction } from 'express'; +import { AppError } from '../errors/app-error'; + +export function notFoundHandler(req: Request, _res: Response, next: NextFunction): void { + next(new AppError(404, `Route introuvable: ${req.method} ${req.originalUrl}`)); +} diff --git a/eme-backend/src/middlewares/request-logger.ts b/eme-backend/src/middlewares/request-logger.ts new file mode 100644 index 0000000..d55436d --- /dev/null +++ b/eme-backend/src/middlewares/request-logger.ts @@ -0,0 +1,12 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from '../utils/logger'; + +export function requestLogger(req: Request, res: Response, next: NextFunction): void { + const start = Date.now(); + + res.on('finish', () => { + logger.info(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`); + }); + + next(); +} diff --git a/eme-backend/src/utils/logger.ts b/eme-backend/src/utils/logger.ts new file mode 100644 index 0000000..e33b0d7 --- /dev/null +++ b/eme-backend/src/utils/logger.ts @@ -0,0 +1,18 @@ +type LogLevel = 'info' | 'warn' | 'error'; + +function log(level: LogLevel, message: string, meta?: unknown): void { + const line = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`; + const write = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log; + + if (meta !== undefined) { + write(line, meta); + } else { + write(line); + } +} + +export const logger = { + info: (message: string, meta?: unknown): void => log('info', message, meta), + warn: (message: string, meta?: unknown): void => log('warn', message, meta), + error: (message: string, meta?: unknown): void => log('error', message, meta), +}; diff --git a/review.md b/review.md index a0200d3..c270528 100644 --- a/review.md +++ b/review.md @@ -98,6 +98,14 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Le modèle n'a pas d'entité "École" : seule `Campus` existe. Un campus = un lieu physique partagé par les deux écoles (et non une école). - MVP : on démarre avec le **seul campus Saint-Christophe (Cergy)** ; les autres campus viendront ensuite. +### Étape 11 — Block 1 : middlewares backend +- Structure en couches : `utils/logger.ts`, `errors/app-error.ts`, `middlewares/` (request-logger, not-found, error-handler). +- Gestionnaire d'erreurs global : une `AppError` renvoie son statut + message ; toute autre erreur devient une 500 (stack exposée uniquement en `development`) et est tracée. +- Logger de requêtes (méthode, URL, statut, durée) + logger applicatif horodaté. +- CORS restreint à `env.frontendUrl` (était ouvert à toutes les origines). `SIGTERM` géré en plus de `SIGINT` (arrêt propre du container Docker). +- Express 5 propage nativement les erreurs des handlers async : pas de wrapper `asyncHandler` nécessaire. +- Vérifié au runtime : `/health` 200, route inconnue 404 en JSON, requêtes journalisées. + --- ## Dette technique en attente @@ -112,4 +120,4 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y --- -*Dernière mise à jour : 2026-06-15 — Fixtures de test en place, Block 2 terminé.* +*Dernière mise à jour : 2026-06-15 — Middlewares backend (gestion d'erreurs, logs) en place.*