1. Variables d'environnement avec dotenv
Les secrets (JWT_SECRET, mots de passe BDD, clés API) ne doivent jamais
être écrits en dur dans le code source ni être commités dans Git.
dotenv charge un fichier .env local dans process.env.
Le problème à éviter
// ❌ DANGER — ne jamais faire ça
const JWT_SECRET = 'mon-super-secret-1234';
const DB_PASSWORD = 'root123';
// ✅ La bonne pratique
const JWT_SECRET = process.env.JWT_SECRET;
const DB_PASSWORD = process.env.DB_PASSWORD;
Installation et configuration
npm install dotenv
Fichier .env (jamais committé)
# .env — fichier LOCAL uniquement, dans .gitignore !
PORT=3000
NODE_ENV=development
JWT_SECRET=un-secret-tres-long-et-aleatoire-genere-avec-openssl
DB_URL=mongodb://localhost:27017/mon-app
ADMIN_PASSWORD=MonMotDePasseAdmin!
# Générer un bon secret : openssl rand -hex 64
Fichier .env.example (committé — template sans vraies valeurs)
# .env.example — committé dans Git comme template
PORT=3000
NODE_ENV=development
JWT_SECRET=change-this-to-a-random-secret-in-production
DB_URL=mongodb://localhost:27017/nom-de-votre-app
ADMIN_PASSWORD=change-this-password
Usage dans server.js — dotenv en premier
// server.js — dotenv.config() DOIT être la première ligne
require('dotenv').config(); // ← en tout premier, avant tout require
const express = require('express'); // ensuite les autres imports
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET;
const NODE_ENV = process.env.NODE_ENV || 'development';
if (!JWT_SECRET) {
console.error('ERREUR : JWT_SECRET non défini. Copiez .env.example vers .env');
process.exit(1); // Arrêter si une variable critique manque
}
console.log(`Serveur en mode : ${NODE_ENV}`);
.gitignore
# .gitignore — ne jamais oublier ces lignes
node_modules/
.env
.env.local
.env.production
*.log
.env avec de vrais secrets,
changez immédiatement toutes les valeurs exposées. L'historique Git conserve le fichier même après suppression.
2. helmet — Sécuriser les headers HTTP
helmet est un middleware Express qui ajoute automatiquement une quinzaine de headers
HTTP de sécurité. Ces headers protègent contre des attaques courantes comme le clickjacking,
le MIME sniffing, et les injections de scripts.
Installation et usage minimal
npm install helmet
const express = require('express');
const helmet = require('helmet');
const app = express();
// Une seule ligne protège contre ~14 vecteurs d'attaque
app.use(helmet());
// Equivalent à appeler manuellement :
// app.use(helmet.contentSecurityPolicy())
// app.use(helmet.crossOriginEmbedderPolicy())
// app.use(helmet.dnsPrefetchControl())
// app.use(helmet.frameguard()) ← anti-clickjacking
// app.use(helmet.hidePoweredBy()) ← cache "X-Powered-By: Express"
// app.use(helmet.hsts()) ← force HTTPS
// app.use(helmet.ieNoOpen())
// app.use(helmet.noSniff()) ← anti-MIME-sniffing
// app.use(helmet.xssFilter()) ← anti-XSS basique
Headers ajoutés par helmet
| Header | Protection |
|---|---|
X-Content-Type-Options: nosniff | Empêche le MIME sniffing |
X-Frame-Options: SAMEORIGIN | Empêche le clickjacking (iframes) |
Strict-Transport-Security | Force HTTPS (HSTS) |
X-XSS-Protection: 0 | Désactive le filtre XSS buggy des vieux browsers |
X-Powered-By (supprimé) | Cache la technologie utilisée |
Referrer-Policy | Contrôle les infos de referrer envoyées |
Configuration avancée CSP
app.use(helmet({
// Désactiver CSP si vous avez votre propre config meta HTML
contentSecurityPolicy: false,
// Ou configurer finement :
// contentSecurityPolicy: {
// directives: {
// defaultSrc: ["'self'"],
// scriptSrc: ["'self'", "https://cdnjs.cloudflare.com"],
// }
// }
}));
3. cors — Cross-Origin Resource Sharing
Par sécurité, les navigateurs bloquent les requêtes JavaScript vers un domaine différent
de celui de la page. cors permet de configurer quels origines ont le droit
d'appeler votre API.
Installation
npm install cors
cors permissif (développement uniquement)
const cors = require('cors');
// ⚠️ Autorise TOUT — OK pour le dev local, JAMAIS en prod
app.use(cors());
// Equivalent à : Access-Control-Allow-Origin: *
cors restrictif (production)
const cors = require('cors');
// Liste blanche d'origines autorisées
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [
'https://monapp.com',
'https://www.monapp.com',
];
app.use(cors({
origin(origin, callback) {
// Autoriser les requêtes sans origin (ex: curl, Postman)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS bloqué : ${origin} non autorisé`));
}
},
credentials: true, // Autoriser les cookies cross-origin
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400, // Cache preflight 24h (réduit les OPTIONS)
}));
// .env
// ALLOWED_ORIGINS=https://monapp.com,http://localhost:5173
app.get('/api/public', cors(), handler) —
vous pouvez appliquer cors différemment selon les routes (tout ouvrir sur /api/public, restreindre sur /api/private).
4. Rate Limiting — Protection contre les abus
Sans rate limiting, un attaquant peut tenter des milliers de mots de passe (force brute),
faire du scraping massif ou saturer votre serveur (DoS). express-rate-limit
limite le nombre de requêtes par IP sur une fenêtre de temps.
Installation
npm install express-rate-limit
Rate limiter global
const rateLimit = require('express-rate-limit');
// Limite globale : 100 requêtes par 15 minutes par IP
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes en millisecondes
max: 100, // max 100 requêtes par fenêtre
standardHeaders: true, // Retourne RateLimit-* headers (RFC 6585)
legacyHeaders: false, // Désactiver X-RateLimit-* headers
message: {
success: false,
error: 'Trop de requêtes depuis cette IP, réessayez dans 15 minutes'
},
});
app.use(globalLimiter); // Appliqué à toutes les routes
Rate limiter strict pour les routes auth
// Limite plus stricte pour les tentatives de connexion
const authLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 5, // max 5 tentatives par minute
message: {
success: false,
error: 'Trop de tentatives de connexion, réessayez dans 1 minute'
},
skipSuccessfulRequests: true, // Ne compte pas les requêtes réussies
});
// Appliquer uniquement aux routes d'auth
app.use('/auth', authLimiter);
app.use('/auth/login', authLimiter);
app.use('/auth/register', authLimiter);
Headers retournés au client
| Header | Valeur exemple | Description |
|---|---|---|
RateLimit-Limit | 100 | Limite maximale de la fenêtre |
RateLimit-Remaining | 87 | Requêtes restantes dans la fenêtre |
RateLimit-Reset | 1634567890 | Timestamp de réinitialisation |
Retry-After | 60 | Secondes avant de réessayer (si bloqué) |
5. Bonnes pratiques production
Un serveur Express prêt pour la production nécessite plus que du code fonctionnel. Voici les patterns essentiels pour un déploiement robuste et sécurisé.
asyncHandler — Éviter les try/catch répétitifs
// Wrapper qui capture les erreurs des fonctions async
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
// Sans asyncHandler (verbeux)
app.get('/users', async (req, res, next) => {
try {
const users = await db.getAll();
res.json(users);
} catch (err) {
next(err); // envoyer au error handler
}
});
// Avec asyncHandler (propre)
app.get('/users', asyncHandler(async (req, res) => {
const users = await db.getAll(); // les erreurs sont auto-catchées
res.json(users);
}));
Error handler production-ready
// Error handler Express — 4 paramètres obligatoires
app.use((err, req, res, next) => {
const isProd = process.env.NODE_ENV === 'production';
const status = err.status || err.statusCode || 500;
// Logger en dev mais pas les stack traces en prod
if (!isProd) {
console.error(err.stack);
} else {
console.error(JSON.stringify({
time: new Date().toISOString(),
status,
message: err.message,
path: req.path,
method: req.method,
}));
}
res.status(status).json({
success: false,
// En prod : ne jamais exposer les détails d'une erreur 500
error: isProd && status === 500 ? 'Erreur interne du serveur' : err.message,
// Stack trace uniquement en dev
...(isProd ? {} : { stack: err.stack }),
});
});
Checklist déploiement production
NODE_ENV=production— active les optimisations Express- JWT_SECRET long et aléatoire (
openssl rand -hex 64) - HTTPS obligatoire — utiliser un reverse proxy (nginx, Caddy, Cloudflare)
- Ne jamais exposer les stack traces en production
.envdans.gitignore— toujours- Rate limiting activé sur les routes auth
- helmet() activé pour les headers de sécurité
- cors() configuré avec une liste blanche d'origines
- Logs structurés (JSON) pour faciliter le monitoring
- Validation des entrées sur toutes les routes publiques
Structurer les erreurs avec un statut
// Créer des erreurs avec un code HTTP
function createError(message, status) {
const err = new Error(message);
err.status = status;
return err;
}
// Utilisation avec asyncHandler
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.findById(req.params.id);
if (!user) throw createError('Utilisateur introuvable', 404);
if (user.id !== req.user.userId) throw createError('Accès interdit', 403);
res.json({ success: true, user });
}));