feat(backend): add global error handling and request logging middlewares
This commit is contained in:
+20
-7
@@ -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<void> {
|
||||
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<void> => {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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}`));
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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.*
|
||||
|
||||
Reference in New Issue
Block a user