Faire tourner une API Express en production ne se limite pas à démarrer un serveur. Il faut des valeurs par défaut sécurisées, des déploiements fiables, une visibilité approfondie, des sauvegardes testées et un plan pour les mises à jour et les incidents. Cette liste de contrôle pratique fournit des exemples copier-coller et des critères de sortie concrets pour que votre équipe puisse opérer en toute confiance.

## Vue d'ensemble du flux de travail

Adoptez un flux explicite et par étapes afin que chaque étape ait des responsables clairs et des critères de sortie :

- Planifier

- Définir les SLO (Service Level Objectives) : disponibilité (par exemple 99,9 %), latence p95 < 200 ms, budget d'erreur de 0,1 %.

- Choisir l'environnement d'exécution : Node LTS, version d'Express, gestionnaire de processus.

- Décider de la pile d'observabilité : logs, métriques, traces et cibles d'alerte.

- Durcir

- Verrouiller la configuration d'environnement et les secrets.

- Ajouter les middlewares de sécurité, les timeouts et l'arrêt gracieux.

- Observer

- Émettre des logs JSON avec identifiants de requête.

- Exposer /healthz, /readyz et /metrics.

- Vérifier

- Tests de charge jusqu'aux SLO.

- Bases du chaos : terminer un processus ; vérifier qu'aucune requête n'est perdue.

- Exécuter un exercice de sauvegarde et restauration.

- Déployer

- Déploiement sans temps d'arrêt (drain, swap ou rolling).

- Surveiller pendant 30 minutes ; guetter les régressions.

- Opérer

- Runbooks d'astreinte pour latence élevée, pics d'erreurs, pression disque et défaillances de dépendances.

Une liste de contrôle concise et revue réduit le retravail et augmente la cohérence entre les versions.

## Configuration et durcissement

Appliquez ces valeurs par défaut avant votre première requête en production.

- Node et Express

- Utilisez Node LTS et épinglez-le via engines dans package.json.

- Définissez NODE_ENV=production .

- Évitez le travail CPU synchrone et lourd dans les chemins de requête.

- Middlewares de sécurité et limites

- helmet() pour les en-têtes courants.

- compression() seulement si l'amont ne compresse pas déjà.

- express.json / urlencoded avec limites de taille strictes (ex. 1 Mo).

- express-rate-limit ou limitation de débit au niveau de la passerelle.

- Désactiver x-powered-by et valider CORS avec une liste d'autorisation.

- Trust proxy

- Si derrière un reverse proxy ou un load balancer, définissez app.set('trust proxy', true) ou un sous-réseau pour que req.ip et le protocole soient corrects.

- Timeouts et en-têtes

- Les timeouts keep-alive et d'en-têtes préviennent les sockets bloqués et le slowloris.

- Définissez toujours un timeout de requête côté serveur (ex. 30 secondes).

- Erreurs et 404

- Gestionnaire d'erreurs centralisé avec forme JSON cohérente et pas de stack traces vers les clients.

- Arrêt gracieux

- Gérer SIGTERM et SIGINT pour drainer, puis fermer le serveur. Refuser le trafic en basculant la readiness avant l'arrêt.

Serveur de démarrage pratique (à adapter selon vos besoins) :

// server.js
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const pino = require('pino');
const pinoHttp = require('pino-http');
const crypto = require('crypto');

const app = express();
app.set('env', process.env.NODE_ENV || 'production');
app.disable('x-powered-by');
app.set('trust proxy', true); // si derrière un proxy

app.use(helmet());
app.use(compression());
app.use(express.json({ limit: '1mb', strict: true }));
app.use(express.urlencoded({ extended: false, limit: '1mb' }));

// Identifiant de requête
app.use((req, res, next) => {
 const rid = req.headers['x-request-id'] || crypto.randomUUID();
 res.setHeader('x-request-id', rid);
 req.id = rid;
 next();
});

const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
app.use(pinoHttp({ logger, genReqId: req => req.id }));

// Limitation de débit (ajuster par endpoint)
app.use(rateLimit({ windowMs: 60_000, max: 300, standardHeaders: true, legacyHeaders: false }));

// Middleware de timeout de requête (exemple : 30s)
app.use((req, res, next) => {
 const timeout = 30_000;
 const timer = setTimeout(() => {
 if (!res.headersSent) {
 res.status(504).json({ error: 'Gateway Timeout' });
 }
 }, timeout);
 res.on('finish', () => clearTimeout(timer));
 res.on('close', () => clearTimeout(timer));
 next();
});

// Santé et disponibilité
let isReady = false;
app.get('/healthz', (req, res) => res.status(200).json({ ok: true }));
app.get('/readyz', (req, res) => isReady ? res.json({ ready: true }) : res.status(503).json({ ready: false }));

// Route d'exemple
app.get('/v1/ping', (req, res) => res.json({ pong: true }));

// 404 et gestionnaire d'erreurs
app.use((req, res) => res.status(404).json({ error: 'Not Found' }));
app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
 req.log.error({ err }, 'Erreur non gérée');
 res.status(500).json({ error: 'Internal Server Error' });
});

const server = app.listen(process.env.PORT || 3000, () => {
 logger.info({ port: server.address().port }, 'serveur démarré');
 isReady = true;
});

// Timeouts et arrêt gracieux
server.keepAliveTimeout = 61_000; // > keepalive du LB
server.headersTimeout = 65_000; // > keepAliveTimeout

const shutdown = () => {
 logger.info('arrêt commencé');
 isReady = false;
 server.close(err => {
 if (err) logger.error({ err }, 'erreur fermeture serveur');
 logger.info('arrêt terminé');
 process.exit(err ? 1 : 0);
 });
 setTimeout(() => {
 logger.error('arrêt forcé');
 process.exit(1);
 }, 30_000).unref();
};

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
process.on('unhandledRejection', err => { logger.error({ err }, 'unhandledRejection'); shutdown(); });
process.on('uncaughtException', err => { logger.error({ err }, 'uncaughtException'); shutdown(); }); 

## Observabilité et surveillance

 Rendez chaque requête observable et alertez sur les symptômes visibles par l'utilisateur.

- Journalisation

- Logs JSON, une ligne par événement, avec champs : ts , level , msg , request_id , method , route , status , latency_ms , user_id (si disponible).

- Ne journalisez pas de secrets ni de données personnelles.

- Métriques

- Exposez /metrics au format Prometheus.

- Suivez le nombre de requêtes, le nombre d'erreurs et les histogrammes de latence par méthode/route/statut.

- Traçage

- Propagez x-request-id (et les en-têtes de trace si vous utilisez un traceur). Échantillonnez à un faible pourcentage pour les services à fort QPS.

- Alertes (alignées aux SLO)

- Latence p95 au-dessus du seuil pendant 5 min.

- Taux 5xx au-dessus du seuil pendant 5 min.

- Échec de la readiness sur plus d'une instance.

- Pression disque ou mémoire près des limites.

Exemple de métriques avec prom-client :

const client = require('prom-client');
client.collectDefaultMetrics();
const httpLatency = new client.Histogram({
 name: 'http_request_duration_ms',
 help: 'Latence des requêtes HTTP en ms',
 labelNames: ['method', 'route', 'status_code'],
 // Buckets adaptés à la latence API typique : 5 ms à 2 s
 buckets: [5, 15, 50, 100, 300, 500, 1000, 2000]
});

app.use((req, res, next) => {
 const start = Date.now();
 res.on('finish', () => {
 const route = req.route && req.route.path ? req.route.path : req.path;
 httpLatency.labels(req.method, route, String(res.statusCode)).observe(Date.now() - start);
 });
 next();
});

app.get('/metrics', async (req, res) => {
 res.set('Content-Type', client.register.contentType);
 res.end(await client.register.metrics());
}); 

## Opérations d'exécution

 Opérez pour le zéro temps d'arrêt et un comportement prévisible.

- Gestionnaire de processus

- Utilisez un superviseur (par exemple, un gestionnaire de services ou PM2) pour redémarrer sur crash et gérer les instances.

- Préférez un processus par CPU avec arrêt gracieux. Évitez le clustering en processus si votre orchestrateur gère les replicas.

- Reverse proxy

- Terminez le TLS, définissez les en-têtes proxy ( X-Forwarded-* ) et gardez le keep-alive activé.

Extrait NGINX d'exemple :

upstream api {
 server 127.0.0.1:3000;
 keepalive 64;
}
server {
 listen 443 ssl http2;
 server_name api.example.com;
 location / {
 proxy_pass http://api;
 proxy_http_version 1.1;
 proxy_set_header Connection '';
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
 proxy_set_header X-Forwarded-Host $host;
 proxy_set_header X-Request-Id $request_id;
 proxy_read_timeout 30s;
 }
} 

- Déploiement sans temps d'arrêt

- Marquer l'instance comme non prête ( readyz -> 503).

- Drainer les connexions via le load balancer.

- Arrêter le processus après la fin de server.close .

- Démarrer la nouvelle instance, attendre readyz =200, puis l'ajouter au pool.

- Limites de ressources

- Définissez ulimit -n (descripteurs de fichiers) assez haut pour les sockets concurrentes.

- Ajustez le heap Node via --max-old-space-size si nécessaire ; surveillez le GC.

- Mise à l'échelle

- Gardez l'API sans état ; utilisez des stockages externes pour les sessions ou le cache.

- Pour les sessions collantes (si inévitable), assurez la persistance du LB et la limitation de débit par client.

## Gestion des données

 Votre API n'est fiable que par sa dernière restauration vérifiée.

- Connexions MongoDB

- Utilisez l'URI SRV avec TLS, ajustez maxPoolSize à la concurrence.

- Timeouts : serverSelectionTimeoutMS et socketTimeoutMS .

- Sauvegardes

- Planifiez les dumps et conservez-les hors hôte avec rétention.

- Testez la restauration régulièrement vers une base de staging.

Exemples :

# Sauvegarde
mongodump --uri "$MONGODB_URI" \
 --gzip --archive=/backups/$(date +%F).gz

# Restauration (vers une base de test)
mongorestore --uri "$MONGODB_RESTORE_URI" \
 --gzip --archive=/backups/2024-01-15.gz --drop 

- Migrations

- Scripts de migration versionnés et idempotents.

- Exécutez les migrations avant d'activer les nouveaux chemins de code.

- Ajoutez des index TTL pour l'expiration automatique des données.

## Maintenance et mises à niveau

 Gardez la vélocité sans casser la production.

- Dépendances

- Verrouillez avec package-lock et mettez à jour selon un calendrier.

- Surveillez les avis de sécurité et mettez à jour rapidement les paquets à risque élevé.

- Node et Express

- Suivez Node LTS et planifiez les mises à jour mineures chaque trimestre.

- Utilisez des canary releases pour les sauts majeurs et surveillez p95/5xx.

- Hygiène opérationnelle

- Faites tourner les secrets et jetons.

- Purgez les logs ; assurez-vous que le disque est surveillé et rotaté.

- Menez des post-mortems d'incident et ajoutez des éléments à la checklist pour éviter les répétitions.

## Plan Pilote Local

Commencez petit, mesurez et itérez. Gardez le premier pilote étroit et facile à inspecter localement avant le déploiement.

Périmètre du pilote :

- Un endpoint ( /v1/ping )

- /healthz et /readyz

- Journalisation JSON avec identifiants de requête

- Métriques Prometheus avec histogramme de latence

- Arrêt gracieux et timeouts

Étapes :

- Échafaudez un serveur Express minimal avec l'exemple de ce guide.

- Ajoutez l'identifiant de requête et la journalisation pino. Vérifiez que les logs montrent request_id et la latence.

- Ajoutez les métriques prom-client et exposez /metrics . Confirmez que les compteurs et histogrammes changent sous charge.

- Implémentez le gating de readiness (basculez isReady au démarrage et à l'arrêt).

- Test de charge avec un petit outil (par exemple, autocannon) jusqu'à votre SLO de latence :

autocannon -c 50 -d 30 http://localhost:3000/v1/ping 

- Envoyez SIGTERM pendant la charge ; vérifiez qu'il n'y a pas de pics 5xx et aucune requête perdue.

- Si vous utilisez MongoDB, connectez-vous avec une petite requête, définissez un index TTL dans une collection de test, et exécutez un cycle de sauvegarde et restauration.

 Critères de sortie :

- Latence p95 dans la cible sous la charge attendue

- Aucune erreur pendant l'arrêt contrôlé

- Métriques et logs complets et utiles pour le débogage

## Pièges courants en production

Évitez ces sources fréquentes d'incidents :

- Pas d'arrêt gracieux : les requêtes tombent au déploiement.

- Timeouts manquants : les connexions pendent et les workers s'épuisent.

- trust proxy non défini : IPs clients incorrectes ; limitation de débit inefficace.

- Payloads illimités : gros corps font planter le processus.

- Blocage de la boucle d'événements : crypto, zlib ou JSON synchrones sur gros payloads.

- Fuites mémoire : rétention req/res ou gros caches sans bornes.

- Prolifération des logs : logs non bornés remplissent le disque ; rotattez et plafonnez la taille.

- Retries non bornés : troupeaux tonnants ; ajoutez un backoff avec gigue et des circuit breakers (ex. opossum).

- Pool Mongo trop grand : timeouts et contention sous charge.

- Secrets dans les logs : nettoyez les champs sensibles.

- Pas de gestionnaire 404 : les défauts du framework fuient les internes.

- Pas de validation d'env : l'application démarre avec de mauvais réglages et échoue sous trafic.

## Conclusion

Une API Express prête pour la production est délibérée : valeurs par défaut sécurisées, observabilité forte, déploiements sûrs, sauvegardes testées et flux de travail répétable. Commencez par le pilote local, passez à un déploiement sans temps d'arrêt, et continuez d'itérer.

Checklist pré-vol :

- [ ] NODE_ENV=production et Node LTS épinglé

- [ ] helmet, compression (si nécessaire), limites de taille, limites de débit

- [ ] trust proxy configuré correctement

- [ ] Timeouts keep-alive et d'en-têtes définis ; timeout de requête appliqué

- [ ] /healthz , /readyz , /metrics exposés et surveillés

- [ ] Logs JSON avec identifiants de requête ; données personnelles nettoyées

- [ ] Arrêt gracieux sur SIGTERM/SIGINT

- [ ] Sauvegardes planifiées et restauration testée

- [ ] Migrations versionnées et idempotentes

- [ ] Alertes sur latence p95, taux d'erreur, readiness et limites de ressources

- [ ] Runbook pour pics de latence et d'erreurs

Adoptez le flux de travail, gardez le pilote étroit, et élargissez avec confiance en prouvant la fiabilité à chaque étape.