Construisez une API NestJS scalable : architecture modulaire, injection de dépendances, controllers, TypeORM PostgreSQL, validation, Swagger et tests Jest.
1. Pourquoi NestJS pour le back-end
NestJS est un framework Node.js progressif construit avec TypeScript et largement inspiré d'Angular. Il propose une architecture modulaire opinion-driven (modules, controllers, providers), un IoC container avec injection de dépendances, et un écosystème de modules officiels (TypeORM, Mongoose, GraphQL, WebSockets, Microservices).
Express vs NestJS : quelle différence concrète ?
| Critère | Express | NestJS |
|---|---|---|
| Architecture | Libre, à structurer soi-même | Modulaire, opinion-driven |
| TypeScript | À configurer manuellement | First-class, decorators natifs |
| Dependency Injection | Aucune (manuelle) | IoC container intégré |
| Validation | Middleware tiers (joi, zod) | ValidationPipe + class-validator |
| Documentation API | Manuelle (Swagger UI à brancher) | @nestjs/swagger générée automatiquement |
| Tests | Mocha/Jest à configurer | Jest + TestingModule prêt à l'emploi |
- Équipe Angular qui veut un back-end avec les mêmes patterns (DI, decorators, modules)
- API d'entreprise avec contrôleurs, services, gestion d'erreurs et logs structurés
- Architecture évolutive (microservices, GraphQL, WebSockets sans refactoring)
- Équipe TypeScript déjà autonome (sinon courbe d'apprentissage notable)
Sous le capot : Express ou Fastify ?
NestJS est un méta-framework : il s'appuie par défaut sur Express, mais peut basculer sur Fastify en changeant l'adapter. Vous gardez tout votre code NestJS, seul le HTTP layer change. Fastify donne typiquement +30% de throughput sur les benchmarks JSON pur.
2. Prérequis et installation
Environnement requis
- Node.js 20 LTS ou supérieur
- npm 10+ ou pnpm 9+
- PostgreSQL 15+ (local ou Docker)
- TypeScript 5+ (installé via le starter NestJS)
- Postman / Insomnia / Bruno pour tester les endpoints
Installer le CLI et créer un projet
# Installer le CLI globalement
npm install -g @nestjs/cli
# Vérifier la version
nest --version
# Créer un nouveau projet
nest new tasks-api
# → choisir npm ou pnpm
# Lancer en dev (hot reload)
cd tasks-api
npm run start:dev
Le serveur écoute par défaut sur http://localhost:3000. Le CLI génère une structure prête à l'emploi avec main.ts, app.module.ts, app.controller.ts et app.service.ts.
Arborescence recommandée pour une API d'entreprise
tasks-api/
├── src/
│ ├── main.ts # Bootstrap de l'application
│ ├── app.module.ts # Module racine
│ ├── common/ # Pipes, filters, guards partagés
│ │ ├── filters/
│ │ │ └── http-exception.filter.ts
│ │ ├── guards/
│ │ │ └── jwt-auth.guard.ts
│ │ └── interceptors/
│ │ └── logging.interceptor.ts
│ ├── config/ # Variables d'env typées
│ │ └── configuration.ts
│ ├── tasks/ # Module métier "tasks"
│ │ ├── tasks.module.ts
│ │ ├── tasks.controller.ts
│ │ ├── tasks.service.ts
│ │ ├── dto/
│ │ │ ├── create-task.dto.ts
│ │ │ └── update-task.dto.ts
│ │ └── entities/
│ │ └── task.entity.ts
│ └── users/ # Module "users"
│ └── ...
├── test/
│ └── tasks.e2e-spec.ts # Tests end-to-end
├── .env
├── .env.example
├── nest-cli.json
├── tsconfig.json
└── package.json
3. Architecture modulaire (modules, providers, controllers)
NestJS organise une application autour de trois briques : modules (regroupent une feature), controllers (gèrent les requêtes HTTP), providers (services injectables qui contiennent la logique métier).
Le module racine
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TasksModule } from './tasks/tasks.module';
import { UsersModule } from './users/users.module';
@Module({
imports: [
// Charge .env de manière globale et type-safe
ConfigModule.forRoot({ isGlobal: true }),
TasksModule,
UsersModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
Un module métier
// src/tasks/tasks.module.ts
import { Module } from '@nestjs/common';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
@Module({
// Controllers HTTP exposés par ce module
controllers: [TasksController],
// Providers (services) instanciés et injectables
providers: [TasksService],
// Exporter le service pour le rendre utilisable dans d'autres modules
exports: [TasksService],
})
export class TasksModule {}
Un controller
// src/tasks/tasks.controller.ts
import { Controller, Get, Post, Body, Param, Delete } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
@Controller('tasks') // Toutes les routes sont préfixées par /tasks
export class TasksController {
// Injection de dépendances par constructeur
constructor(private readonly tasksService: TasksService) {}
@Get()
findAll() {
// Délégation au service : le controller reste mince
return this.tasksService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.tasksService.findOne(id);
}
@Post()
create(@Body() dto: CreateTaskDto) {
return this.tasksService.create(dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.tasksService.remove(id);
}
}
4. Injection de dépendances
L'IoC container de NestJS résout automatiquement le graphe de dépendances au démarrage. Quand un controller demande TasksService dans son constructeur, Nest cherche le provider correspondant dans le module courant ou ses imports.
Service injectable
// src/tasks/tasks.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { CreateTaskDto } from './dto/create-task.dto';
@Injectable() // Marqueur indispensable : le provider devient injectable
export class TasksService {
// Stockage en mémoire pour la démo (remplacé par TypeORM en section 6)
private tasks: Array<{ id: string; title: string; done: boolean }> = [];
findAll() {
return this.tasks;
}
findOne(id: string) {
const task = this.tasks.find((t) => t.id === id);
if (!task) {
// Exception HTTP traduite automatiquement en 404 par Nest
throw new NotFoundException(`Task ${id} introuvable`);
}
return task;
}
create(dto: CreateTaskDto) {
const task = { id: randomUUID(), title: dto.title, done: false };
this.tasks.push(task);
return task;
}
remove(id: string) {
const before = this.tasks.length;
this.tasks = this.tasks.filter((t) => t.id !== id);
if (this.tasks.length === before) {
throw new NotFoundException(`Task ${id} introuvable`);
}
return { deleted: id };
}
}
Providers personnalisés (custom tokens)
Pour injecter une valeur (config, instance de tiers, mock), on utilise un token personnalisé :
// src/tasks/tasks.module.ts
import { Module } from '@nestjs/common';
import { TasksService } from './tasks.service';
@Module({
providers: [
TasksService,
{
// Token symbolique
provide: 'TASKS_CONFIG',
// Factory qui construit la valeur
useFactory: () => ({
maxTasksPerUser: 100,
defaultStatus: 'todo',
}),
},
],
})
export class TasksModule {}
// Utilisation dans le service :
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class TasksService {
constructor(
@Inject('TASKS_CONFIG')
private readonly config: { maxTasksPerUser: number; defaultStatus: string },
) {}
}
@Inject(TOKEN) avec useFactory en Angular. La syntaxe est identique parce que les deux frameworks partagent l'inspiration Angular DI.
5. Créer une API CRUD complète
Construisons une ressource Task avec les cinq endpoints classiques. Le CLI Nest génère le scaffold en une commande :
# Génère module + controller + service + DTO + entity + tests
nest g resource tasks --no-spec=false
# → choisir REST API
# → générer les CRUD endpoints : Yes
DTO d'entrée
// src/tasks/dto/create-task.dto.ts
import { IsString, IsBoolean, IsOptional, MinLength, MaxLength } from 'class-validator';
export class CreateTaskDto {
// Validé automatiquement par ValidationPipe (voir section 7)
@IsString()
@MinLength(3, { message: 'Le titre doit faire au moins 3 caractères' })
@MaxLength(120)
title: string;
@IsBoolean()
@IsOptional()
done?: boolean;
}
// src/tasks/dto/update-task.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateTaskDto } from './create-task.dto';
// PartialType rend tous les champs optionnels (PATCH)
export class UpdateTaskDto extends PartialType(CreateTaskDto) {}
Controller complet
// src/tasks/tasks.controller.ts
import {
Controller, Get, Post, Patch, Delete,
Body, Param, HttpCode, ParseUUIDPipe,
} from '@nestjs/common';
import { TasksService } from './tasks.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
findAll() {
return this.tasksService.findAll();
}
@Get(':id')
// ParseUUIDPipe valide le format UUID avant d'atteindre le service
findOne(@Param('id', new ParseUUIDPipe()) id: string) {
return this.tasksService.findOne(id);
}
@Post()
@HttpCode(201) // Statut HTTP explicite (par défaut 201 sur POST)
create(@Body() dto: CreateTaskDto) {
return this.tasksService.create(dto);
}
@Patch(':id')
update(
@Param('id', new ParseUUIDPipe()) id: string,
@Body() dto: UpdateTaskDto,
) {
return this.tasksService.update(id, dto);
}
@Delete(':id')
@HttpCode(204) // 204 No Content après suppression réussie
remove(@Param('id', new ParseUUIDPipe()) id: string) {
return this.tasksService.remove(id);
}
}
Endpoints exposés
| Méthode | Route | Description | Status code |
|---|---|---|---|
| GET | /tasks | Liste toutes les tâches | 200 |
| GET | /tasks/:id | Récupère une tâche par UUID | 200 / 404 |
| POST | /tasks | Crée une tâche | 201 |
| PATCH | /tasks/:id | Met à jour une tâche | 200 / 404 |
| DELETE | /tasks/:id | Supprime une tâche | 204 / 404 |
6. Intégrer TypeORM et PostgreSQL
Le stockage en mémoire n'est utile que pour les tests rapides. Branchons une vraie base PostgreSQL via TypeORM, l'ORM le plus utilisé dans l'écosystème NestJS.
Installation des dépendances
npm install @nestjs/typeorm typeorm pg
npm install --save-dev @types/pg
Configuration via .env
# .env
DB_HOST=localhost
DB_PORT=5432
DB_USER=nest_user
DB_PASSWORD=changeme
DB_NAME=tasks_db
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TasksModule } from './tasks/tasks.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
// Configuration TypeORM asynchrone : injecte ConfigService
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('DB_HOST'),
port: config.get('DB_PORT'),
username: config.get('DB_USER'),
password: config.get('DB_PASSWORD'),
database: config.get('DB_NAME'),
autoLoadEntities: true,
// synchronize: true en dev seulement, jamais en prod
synchronize: process.env.NODE_ENV !== 'production',
}),
}),
TasksModule,
],
})
export class AppModule {}
Entité Task
// src/tasks/entities/task.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('tasks')
export class Task {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 120 })
title: string;
@Column({ default: false })
done: boolean;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}
Repository injecté dans le service
// src/tasks/tasks.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Task } from './entities/task.entity';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
@Module({
// Enregistre le repository Task dans ce module
imports: [TypeOrmModule.forFeature([Task])],
controllers: [TasksController],
providers: [TasksService],
})
export class TasksModule {}
// src/tasks/tasks.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Task } from './entities/task.entity';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
@Injectable()
export class TasksService {
constructor(
// Injection du repository TypeORM lié à l'entité Task
@InjectRepository(Task)
private readonly repo: Repository,
) {}
findAll() {
return this.repo.find({ order: { createdAt: 'DESC' } });
}
async findOne(id: string) {
const task = await this.repo.findOneBy({ id });
if (!task) throw new NotFoundException(`Task ${id} introuvable`);
return task;
}
create(dto: CreateTaskDto) {
// create() instancie sans persister, save() écrit en BDD
const task = this.repo.create(dto);
return this.repo.save(task);
}
async update(id: string, dto: UpdateTaskDto) {
const task = await this.findOne(id); // 404 si introuvable
Object.assign(task, dto);
return this.repo.save(task);
}
async remove(id: string) {
const result = await this.repo.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`Task ${id} introuvable`);
}
}
}
synchronize: true est dangereux en prod (il modifie le schéma à chaque démarrage). Utilisez typeorm migration:generate et migration:run pour gérer les évolutions de schéma de manière contrôlée.
7. Validation avec class-validator et Pipes
NestJS valide automatiquement les DTO d'entrée si vous activez le ValidationPipe global. Toute requête avec un payload invalide retourne un 400 sans atteindre le controller.
Activer la validation globale
npm install class-validator class-transformer
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Retire les champs non déclarés dans le DTO
forbidNonWhitelisted: true, // 400 si champ inconnu envoyé
transform: true, // Convertit primitives ('5' → 5, 'true' → true)
transformOptions: {
enableImplicitConversion: true,
},
}),
);
// Préfixe global /api/v1
app.setGlobalPrefix('api/v1');
await app.listen(3000);
}
bootstrap();
Validation enrichie sur un DTO
// src/tasks/dto/create-task.dto.ts
import {
IsString, IsBoolean, IsOptional,
IsEnum, MinLength, MaxLength, IsInt, Min, Max,
} from 'class-validator';
export enum TaskPriority {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
}
export class CreateTaskDto {
@IsString()
@MinLength(3)
@MaxLength(120)
title: string;
@IsOptional()
@IsString()
@MaxLength(1000)
description?: string;
@IsOptional()
@IsEnum(TaskPriority, { message: 'priority doit être low, medium ou high' })
priority?: TaskPriority;
@IsOptional()
@IsInt()
@Min(1)
@Max(100)
estimatedHours?: number;
@IsOptional()
@IsBoolean()
done?: boolean;
}
Exemple de réponse d'erreur
{
"statusCode": 400,
"message": [
"title must be longer than or equal to 3 characters",
"priority doit être low, medium ou high"
],
"error": "Bad Request"
}
8. Guards, Interceptors et Middlewares
NestJS découpe le pipeline de requête en couches spécialisées. Chacune a un rôle précis : ne pas mélanger.
| Couche | Rôle | Cas d'usage |
|---|---|---|
| Middleware | Logique avant le routing | Logger HTTP brut, cookies, body parser |
| Guard | Autoriser ou refuser une requête | Auth JWT, RBAC, IP whitelist |
| Interceptor | Transformer requête / réponse | Logging, cache, sérialisation, mesure de durée |
| Pipe | Transformer / valider un paramètre | ValidationPipe, ParseIntPipe, ParseUUIDPipe |
| Exception Filter | Catcher les exceptions | Format JSON d'erreur standardisé, Sentry |
Guard JWT simple
// src/common/guards/jwt-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
import * as jwt from 'jsonwebtoken';
@Injectable()
export class JwtAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
throw new UnauthorizedException('Token manquant');
}
try {
const token = auth.slice(7);
const payload = jwt.verify(token, process.env.JWT_SECRET as string);
// Attache l'utilisateur à la requête pour les handlers suivants
(req as any).user = payload;
return true;
} catch {
throw new UnauthorizedException('Token invalide ou expiré');
}
}
}
// Utilisation sur un controller
// @UseGuards(JwtAuthGuard)
// @Controller('tasks')
// export class TasksController { ... }
Interceptor de logging
// src/common/interceptors/logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable {
const req = context.switchToHttp().getRequest();
const start = Date.now();
return next.handle().pipe(
// tap permet d'observer sans modifier la réponse
tap(() => {
const ms = Date.now() - start;
this.logger.log(`${req.method} ${req.url} - ${ms}ms`);
}),
);
}
}
Filter global pour normaliser les erreurs
// src/common/filters/http-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const res = ctx.getResponse();
const req = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';
// Format de réponse standardisé pour le front Angular
res.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: req.url,
error: message,
});
}
}
9. Documentation Swagger automatique
NestJS génère une documentation OpenAPI complète à partir des decorators du code. Plus besoin de maintenir un fichier YAML à part.
npm install @nestjs/swagger swagger-ui-express
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Tasks API')
.setDescription('API CRUD construite avec NestJS')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
// UI accessible sur http://localhost:3000/docs
SwaggerModule.setup('docs', app, document);
await app.listen(3000);
}
bootstrap();
Enrichir la doc via decorators
// src/tasks/dto/create-task.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength } from 'class-validator';
export class CreateTaskDto {
@ApiProperty({
description: 'Titre court de la tâche',
example: 'Préparer la release v2',
minLength: 3,
maxLength: 120,
})
@IsString()
@MinLength(3)
title: string;
}
// src/tasks/tasks.controller.ts
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
@ApiTags('tasks')
@Controller('tasks')
export class TasksController {
@Post()
@ApiOperation({ summary: 'Créer une nouvelle tâche' })
@ApiResponse({ status: 201, description: 'Tâche créée' })
@ApiResponse({ status: 400, description: 'Payload invalide' })
create(@Body() dto: CreateTaskDto) {
return this.tasksService.create(dto);
}
}
openapi-generator), tests manuels via l'UI, contrat partagé avec l'équipe front, validation continue de l'API en CI.
10. Tests unitaires et e2e
NestJS embarque Jest et un TestingModule qui simule le container DI réel. Les tests sont rapides, isolés et reflètent la production.
Test unitaire d'un service avec repository mocké
// src/tasks/tasks.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotFoundException } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { Task } from './entities/task.entity';
describe('TasksService', () => {
let service: TasksService;
let repo: jest.Mocked>;
beforeEach(async () => {
// Mock minimal du repository TypeORM
const repoMock: Partial>> = {
find: jest.fn(),
findOneBy: jest.fn(),
create: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
TasksService,
{ provide: getRepositoryToken(Task), useValue: repoMock },
],
}).compile();
service = module.get(TasksService);
repo = module.get(getRepositoryToken(Task)) as jest.Mocked>;
});
it('findOne lève NotFoundException quand la tâche n\'existe pas', async () => {
repo.findOneBy.mockResolvedValue(null);
await expect(service.findOne('uuid-inexistant'))
.rejects.toBeInstanceOf(NotFoundException);
});
it('create persiste une nouvelle tâche', async () => {
const dto = { title: 'Test' };
const created = { id: 'uuid-1', title: 'Test', done: false } as Task;
repo.create.mockReturnValue(created);
repo.save.mockResolvedValue(created);
const result = await service.create(dto);
expect(repo.create).toHaveBeenCalledWith(dto);
expect(repo.save).toHaveBeenCalledWith(created);
expect(result).toEqual(created);
});
});
Test end-to-end avec Supertest
// test/tasks.e2e-spec.ts
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Tasks (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
});
afterAll(async () => {
await app.close();
});
it('POST /api/v1/tasks retourne 400 si title manquant', () => {
return request(app.getHttpServer())
.post('/api/v1/tasks')
.send({})
.expect(400);
});
it('POST /api/v1/tasks crée une tâche valide', () => {
return request(app.getHttpServer())
.post('/api/v1/tasks')
.send({ title: 'Tâche e2e' })
.expect(201)
.expect((res) => {
expect(res.body.title).toBe('Tâche e2e');
expect(res.body.done).toBe(false);
});
});
});
Commandes
npm run test # Unitaires
npm run test:watch # Mode watch
npm run test:cov # Couverture
npm run test:e2e # Tests end-to-end
11. Déploiement en production
Dockerfile multi-stage
# Stage 1 : build TypeScript
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2 : runtime minimal
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 3000
CMD ["node", "dist/main.js"]
docker-compose avec PostgreSQL
version: '3.9'
services:
api:
build: .
ports:
- "3000:3000"
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USER: nest_user
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: tasks_db
JWT_SECRET: ${JWT_SECRET}
NODE_ENV: production
depends_on:
- postgres
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: nest_user
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: tasks_db
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Hardening production
// src/main.ts (production-ready)
import helmet from 'helmet';
import * as compression from 'compression';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Sécurité HTTP : Content-Security-Policy, HSTS, X-Frame-Options...
app.use(helmet());
app.use(compression());
app.enableCors({
origin: process.env.CORS_ORIGIN?.split(',') ?? [],
credentials: true,
});
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.setGlobalPrefix('api/v1');
// Graceful shutdown : ferme les connexions DB proprement
app.enableShutdownHooks();
await app.listen(process.env.PORT || 3000);
}
bootstrap();
pm2 start dist/main.js -i max) pour profiter du clustering Node natif et du restart automatique. Voir l'article dédié PM2.
Conclusion et checklist
NestJS est aujourd'hui le framework Node.js d'entreprise le plus mûr. Sa structure modulaire, son IoC container et son écosystème (TypeORM, Swagger, GraphQL, microservices) en font un choix naturel pour les équipes Angular ou pour toute API destinée à grandir au-delà du prototype.
✅ Checklist mise en production
- Variables d'environnement chargées via
@nestjs/configet validées ValidationPipeglobal avecwhitelistetforbidNonWhitelistedhelmetetcompressionactivés- CORS restreint aux domaines autorisés
- JWT signé avec un secret long, refresh token séparé
- Migrations TypeORM versionnées (jamais
synchronize: trueen prod) - Logger structuré (Pino ou Winston) + Sentry pour les erreurs
- Tests unitaires et e2e dans la CI
- Swagger restreint ou désactivé en prod selon le contexte
- Health check (
@nestjs/terminus) exposé pour Kubernetes - Graceful shutdown activé (
enableShutdownHooks) - Image Docker multi-stage, image finale < 200 Mo
- Framework : NestJS 10 + TypeScript 5
- HTTP adapter : Express (ou Fastify pour +30% throughput)
- ORM : TypeORM + PostgreSQL 16
- Validation : class-validator + ValidationPipe global
- Documentation : @nestjs/swagger
- Tests : Jest + Supertest
- Infrastructure : Docker multi-stage + PM2 ou Kubernetes