Passer une application NestJS du mode développement à la production demande bien plus que de lancer npm start sur un serveur. Entre la sécurité, les migrations de base de données, le logging, la gestion des erreurs et l'orchestration des processus, il existe une véritable checklist à respecter pour éviter les mauvaises surprises. Dans ce guide, nous allons parcourir étape par étape les points essentiels pour déployer sereinement une application NestJS, en s'appuyant sur les patterns réels du projet Le Murmure des Cartes.
1. Désactiver synchronize et passer aux migrations TypeORM
La première règle absolue : ne jamais activer synchronize: true en production. Cette option, pratique en développement, modifie automatiquement le schéma SQL pour le faire correspondre aux entités. En production, c'est une porte ouverte à la perte de données. Préférez les migrations versionnées :
// src/database/data-source.ts
import { DataSource } from 'typeorm';
import * as dotenv from 'dotenv';
dotenv.config();
export default new DataSource({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'voyance',
entities: [__dirname + '/entities/*.entity{.ts,.js}'],
migrations: [__dirname + '/migrations/*{.ts,.js}'],
synchronize: false, // OBLIGATOIRE en production
logging: false,
});
Le DataSource exporté ici sert de configuration à la CLI TypeORM. On peut alors générer, exécuter et annuler des migrations :
# Générer une migration à partir des changements d'entités
npx typeorm-ts-node-commonjs migration:generate ./src/database/migrations/AddUserTable -d ./src/database/data-source.ts
# Exécuter les migrations en attente
npx typeorm-ts-node-commonjs migration:run -d ./src/database/data-source.ts
# Annuler la dernière migration
npx typeorm-ts-node-commonjs migration:revert -d ./src/database/data-source.ts
Ajoutez ces commandes dans votre package.json sous des scripts migration:run, migration:revert et migration:generate pour simplifier leur usage en CI/CD.
2. Compiler le TypeScript et lancer en production
NestJS est écrit en TypeScript, mais Node.js ne l'exécute pas nativement. En développement, ts-node compile à la volée, mais en production il faut compiler le code en JavaScript pour des raisons de performance et de stabilité :
# Compiler vers le dossier dist/
npm run build
# Lancer la version compilée
node dist/main.js
Le npm run build exécute nest build, qui produit un dossier dist/ contenant tout le code transpilé. C'est ce point d'entrée que votre process manager (PM2, Docker…) doit lancer.
3. Variables d'environnement et validation au démarrage
En production, l'application ne doit jamais démarrer si une variable critique manque. Le fichier configuration.ts du projet illustre une bonne pratique : valider explicitement les variables requises avant le bootstrap.
function validateEnv() {
const isProduction = process.env.NODE_ENV === 'production';
const required: string[] = ['JWT_SECRET'];
if (isProduction) {
required.push('DB_HOST', 'DB_PASSWORD', 'FRONTEND_URL');
}
const missing = required.filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(
`Variables d'environnement manquantes : ${missing.join(', ')}. L'application ne peut pas démarrer.`,
);
}
}
validateEnv();
Ce mécanisme « fail-fast » évite qu'un service tourne dans un état corrompu (par exemple sans secret JWT). Pensez à stocker ces variables dans un gestionnaire de secrets (Vault, AWS Secrets Manager, variables Docker/Kubernetes) plutôt que dans un fichier .env commité.
4. Sécuriser l'application : helmet, CORS et HTTPS
NestJS expose une API publique : il faut donc activer plusieurs couches de sécurité dès le bootstrap. Le main.ts du projet en montre une configuration solide :
import helmet from 'helmet';
const app = await NestFactory.create(AppModule, {
rawBody: true, // Requis pour les webhooks Stripe
});
app.enableShutdownHooks();
// Headers de sécurité HTTP
app.use(
helmet({
contentSecurityPolicy: false, // Géré côté frontend/Traefik
crossOriginEmbedderPolicy: false, // Nécessaire pour les images uploadées
}),
);
// CORS strict : on liste explicitement les origines autorisées
app.enableCors({
origin: origins,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'stripe-signature'],
credentials: true,
});
Helmet ajoute une dizaine de headers HTTP protecteurs (X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security…). Pour le HTTPS, déléguez la terminaison TLS à un reverse proxy (Traefik, Nginx, Caddy) plutôt que de gérer les certificats dans Node : c'est plus simple et plus sécurisé.
Enfin, ajoutez du rate limiting pour bloquer les attaques par force brute, en utilisant @nestjs/throttler :
import { ThrottlerModule } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([{
ttl: 60_000, // 1 minute
limit: 100, // 100 requêtes max
}]),
],
})
export class AppModule {}
5. Gestion des erreurs sans fuite d'information
Une stack trace renvoyée au client est une mine d'or pour un attaquant. La règle est simple : logger en détail côté serveur, mais répondre de façon générique côté client en production. Le filtre global du projet illustre exactement ce comportement :
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
// ... récupération du status et du message
// Log complet côté serveur (avec stack trace)
if (status >= 500) {
this.logger.error(
`${request.method} ${request.url} → ${status}`,
exception instanceof Error ? exception.stack : String(exception),
);
}
const responseBody: Record<string, any> = { statusCode: status, message, error };
// En production, messages génériques
if (process.env.NODE_ENV === 'production') {
if (status >= 500) responseBody.message = 'Une erreur interne est survenue';
if (status === 401) responseBody.message = 'Non autorisé';
if (status === 403) responseBody.message = 'Accès refusé';
}
response.status(status).json(responseBody);
}
}
Le filtre est ensuite enregistré globalement dans main.ts via app.useGlobalFilters(new AllExceptionsFilter()). Couplez cela à un service de monitoring d'erreurs comme Sentry pour être alerté des 500 en temps réel.
6. Logging structuré et rotation
Le logger natif de NestJS est suffisant pour démarrer, mais en production il est conseillé de passer à un logger structuré comme Pino ou Winston, qui produisent du JSON exploitable par des outils comme Loki, Datadog ou ELK.
import { LoggerModule } from 'nestjs-pino';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
redact: ['req.headers.authorization', 'req.headers.cookie'],
},
}),
],
})
export class AppModule {}
Quelques règles à retenir :
- Niveaux :
infoen production,debuguniquement pour le diagnostic ponctuel. - Format JSON : indispensable pour l'agrégation et la recherche.
- Rotation : si vous écrivez dans des fichiers, déléguez-la à
logrotateou à Docker (--log-opt max-size=10m). - Redaction : masquez les headers sensibles (Authorization, cookies) et les mots de passe.
7. Health checks pour les load balancers
Tout orchestrateur (Kubernetes, ECS, Traefik) a besoin d'un endpoint /health pour savoir si le conteneur est vivant et prêt à recevoir du trafic. NestJS fournit @nestjs/terminus pour cela :
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';
import { Public } from '../auth/decorators/public.decorator';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
) {}
@Public()
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
]);
}
}
Pensez à exclure cet endpoint de l'authentification (via un décorateur @Public()) et du rate limiting, sinon le load balancer pourrait être bloqué.
8. Process manager : PM2 ou Docker
Lancer node dist/main.js directement sur un serveur n'est pas suffisant : si l'application crashe, elle ne redémarre pas. Deux options dominent :
PM2 pour des déploiements traditionnels sur VM :
npm install -g pm2
pm2 start dist/main.js --name murmure-api -i max
pm2 save
pm2 startup
Docker, plus moderne et reproductible. Un Dockerfile multi-stage typique :
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 3002
CMD ["node", "dist/main.js"]
N'oubliez pas l'appel à app.enableShutdownHooks() dans main.ts : il permet à NestJS de fermer proprement les connexions à la base lorsque Docker envoie un SIGTERM.
9. Optimisations : compression et cache
Activez la compression Gzip/Brotli pour réduire la taille des réponses JSON :
import compression from 'compression';
app.use(compression());
Pour les ressources statiques (images uploadées par exemple), positionnez des headers Cache-Control appropriés, idéalement gérés par le reverse proxy plutôt que par Node.
10. Désactiver Swagger en production
Exposer la documentation Swagger en production révèle toute la structure de votre API. Le projet le gère déjà proprement :
if (process.env.NODE_ENV !== 'production') {
// Configuration et setup de Swagger uniquement hors production
SwaggerModule.setup('api/docs', app, document, { /* ... */ });
}
Checklist finale avant déploiement
- ✅
synchronize: falsedans la configuration TypeORM - ✅ Migrations générées, testées et appliquées en CI/CD
- ✅
npm run buildexécuté, lancement vianode dist/main.js - ✅ Variables d'environnement validées au démarrage (fail-fast)
- ✅ Secrets stockés dans un gestionnaire dédié, jamais commités
- ✅ Helmet, CORS strict et rate limiting activés
- ✅ HTTPS via reverse proxy (Traefik / Nginx)
- ✅ Filtre global d'exceptions sans stack trace côté client
- ✅ Logger structuré (Pino/Winston) avec niveau
infoet rotation - ✅ Endpoint
/healthpublic pour les load balancers - ✅
app.enableShutdownHooks()activé - ✅ Process manager (PM2 ou Docker) configuré avec restart automatique
- ✅ Compression activée
- ✅ Swagger désactivé en production
- ✅ Monitoring (Sentry, Datadog…) en place
Conclusion
Mettre en production une application NestJS, c'est avant tout adopter une posture défensive : fail-fast au démarrage, logs détaillés côté serveur, réponses minimales côté client, et processus supervisés. Les bonnes pratiques vues ici — migrations TypeORM, helmet, filtre global d'exceptions, health checks, process manager — forment le socle minimum à respecter.
Pour aller plus loin, explorez l'intégration de Terminus pour des health checks plus fins (Redis, services externes), la mise en place de Redis pour le cache, ou encore l'observabilité via OpenTelemetry. Une application bien préparée est une application qu'on peut déployer le vendredi soir sans crainte.




Commentaires