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
enginesdans 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/urlencodedavec limites de taille strictes (ex. 1 Mo).express-rate-limitou limitation de débit au niveau de la passerelle.- Désactiver
x-powered-byet 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 quereq.ipet 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
/metricsau 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-sizesi 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 :
serverSelectionTimeoutMSetsocketTimeoutMS.
- 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) /healthzet/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_idet 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
isReadyau 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 proxynon 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=productionet 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,/metricsexposé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.