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
⚠️ Si vous avez accidentellement committé un .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

HeaderProtection
X-Content-Type-Options: nosniffEmpêche le MIME sniffing
X-Frame-Options: SAMEORIGINEmpêche le clickjacking (iframes)
Strict-Transport-SecurityForce HTTPS (HSTS)
X-XSS-Protection: 0Désactive le filtre XSS buggy des vieux browsers
X-Powered-By (supprimé)Cache la technologie utilisée
Referrer-PolicyContrô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
💡 cors sur des routes spécifiques : 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

HeaderValeur exempleDescription
RateLimit-Limit100Limite maximale de la fenêtre
RateLimit-Remaining87Requêtes restantes dans la fenêtre
RateLimit-Reset1634567890Timestamp de réinitialisation
Retry-After60Secondes 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
  • .env dans .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 });
}));
← N10 JWT Exercices →