Introduction
NestJS est aujourd'hui la framework backend Node.js la plus utilisée pour construire des applications server-side évolutives, typées et faciles à maintenir. Elle combine les concepts les plus solides de l'ingénierie logicielle — Dependency Injection, modules, architecture en couches — avec la productivité de TypeScript et un écosystème qui couvre pratiquement tous les besoins : REST, GraphQL, WebSocket, microservices, CLI, cron jobs.
Dans ce guide, nous construisons, étape par étape, une REST API complète et prête pour la production : une API de blog avec authentification JWT, refresh token, contrôle d'accès basé sur les rôles (RBAC), validation, upload de fichiers, logging centralisé et gestion structurée des erreurs — exactement la stack derrière la plupart des API NestJS réelles en 2026.
Qu'est-ce que NestJS
NestJS est une framework opinionated construite sur Express (ou, alternativement, Fastify) qui impose une structure précise à l'application : chaque fonctionnalité est organisée en modules, chaque module expose des controllers (gèrent les requêtes HTTP) et des providers (contiennent la logique métier, typiquement des services), reliés entre eux via un système de Dependency Injection intégré et inspiré d'Angular.
Pourquoi l'Utiliser
- TypeScript natif : typage de bout en bout, autocomplétion, refactoring sûr.
- Architecture imposée : contrairement à Express pur, NestJS impose de séparer les responsabilités (controller/service/repository), réduisant le "big ball of mud" typique des projets Node ayant grandi sans structure.
- Dependency Injection intégrée : testabilité élevée, faible couplage, providers interchangeables (utile pour les mocks dans les tests).
- Écosystème mature : modules officiels pour TypeORM, Prisma, Mongoose, Passport, GraphQL, WebSocket, Bull/BullMQ, Swagger, gRPC, microservices.
- Décorateurs déclaratifs : guards, interceptors, pipes et exception filters permettent des cross-cutting concerns (auth, logging, validation) sans polluer la logique métier.
Quand Cela en Vaut la Peine (et Quand Non)
NestJS est rentable lorsque le projet a une complexité qui justifie une structure rigide : équipes de plusieurs développeurs, API destinées à grandir dans le temps, besoin de testabilité élevée, applications d'entreprise avec des exigences de sécurité et de compliance. C'est probablement excessif pour un script ponctuel ou un prototype qui sera jeté dans une semaine — là, Express pur ou Fastify "nu" restent plus rapides à démarrer.
Avantages et Inconvénients
| Avantages | Inconvénients |
|---|---|
| Structure claire et cohérente entre différents projets | Courbe d'apprentissage plus raide qu'Express pur (décorateurs, DI, modules) |
| Testabilité native grâce au DI | Overhead de boilerplate pour les très petits projets |
| Écosystème officiel large et bien maintenu | Plus de "magie" (décorateurs, reflection) par rapport à du code explicite |
| Excellente intégration avec TypeScript et Swagger/OpenAPI | Bundle size et cold start légèrement supérieurs aux frameworks minimalistes (pertinent dans les environnements serverless) |
| Facilite l'adoption de Clean Architecture / hexagonale | Nécessite de la discipline d'équipe pour ne pas abuser de la flexibilité des modules |
Cas Réels
NestJS est utilisée en production par des entreprises comme Adidas, Roche, Autodesk et Decathlon, en plus d'être un choix très courant pour les backends SaaS B2B, les plateformes e-commerce, les systèmes de gestion des utilisateurs et les API de microservices qui doivent communiquer entre elles via gRPC ou des files de messages (RabbitMQ, Kafka).
Prérequis
Avant de commencer, assure-toi d'avoir ces outils installés et de connaître les bases de TypeScript (interfaces, décorateurs, generics) et les concepts REST (verbes HTTP, codes de statut, idempotence).
| Outil | Version recommandée | À quoi il sert |
|---|---|---|
| Node.js | 20.x LTS ou supérieur | Runtime JavaScript sur lequel NestJS s'exécute |
| npm | 10.x (inclus dans Node 20) | Gestion des paquets |
| NestJS CLI | @nestjs/cli 10.x+ | Scaffolding des modules, controllers, services |
| TypeScript | 5.x | Langage dans lequel NestJS et ton app sont écrits |
| VS Code | Dernière version stable | Éditeur avec support natif de TypeScript |
| PostgreSQL | 15.x ou supérieur | Base de données relationnelle pour l'exemple avec TypeORM |
| Docker (optionnel) | 24.x+ | Lancer PostgreSQL localement sans installation native |
# Vérifie les versions installées
node -v
npm -v
# Installe la CLI de NestJS globalement
npm install -g @nestjs/cli
nest --version
Astuce : si tu ne veux pas installer PostgreSQL nativement, lance-le avec Docker :
docker run --name pg-blog -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=blog_db -p 5432:5432 -d postgres:16.
Architecture
Avant d'écrire du code, il est essentiel de comprendre comment NestJS achemine une requête HTTP à travers ses composants principaux. Voici le cycle de vie complet d'une requête :
Client
│
▼
┌─────────────┐
│ Middleware │ (ex. logger, cookie-parser — exécuté avant le routing)
└──────┬───────┘
▼
┌─────────────┐
│ Guard │ (ex. AuthGuard — décide si la requête peut continuer)
└──────┬───────┘
▼
┌─────────────┐
│ Interceptor │ (phase "avant" — ex. logging, transformation de l'input)
└──────┬───────┘
▼
┌─────────────┐
│ Pipe │ (valide et transforme les paramètres entrants)
└──────┬───────┘
▼
┌─────────────┐
│ Controller │ (reçoit la requête, délègue au service)
└──────┬───────┘
▼
┌─────────────┐
│ Service │ (logique métier, appelle les repositories)
└──────┬───────┘
▼
┌─────────────┐
│ Repository / │ (accès aux données — TypeORM, Prisma, Mongoose...)
│ Database │
└──────┬───────┘
▼
┌─────────────┐
│ Interceptor │ (phase "après" — ex. transformation de la réponse)
└──────┬───────┘
▼
┌──────────────────┐
│ Exception Filter │ (intercepte SEULEMENT si une exception est levée)
└──────┬────────────┘
▼
Response
Controller
La couche la plus externe : reçoit les requêtes HTTP, extrait les paramètres/body/query et délègue immédiatement la logique au service correspondant. Un controller ne doit jamais contenir de logique métier — sa seule responsabilité est le mapping HTTP ↔ appel de méthode.
Service
Contient la logique métier proprement dite. C'est un provider injectable,
typiquement marqué avec @Injectable(), et il est injecté dans le controller (ou
dans d'autres services) via le constructeur.
Module
Le module est l'unité organisationnelle de NestJS : il regroupe controllers, providers et
imports d'autres modules. Chaque application a au moins un AppModule racine, et
les fonctionnalités sont typiquement isolées dans des feature modules (ex.
PostsModule, AuthModule, UsersModule).
Provider
Toute classe gérée par le conteneur de Dependency Injection de Nest : services,
repositories, factories, helpers. Il est déclaré dans providers au sein du
module et peut être injecté partout où il est disponible (dans le même module, ou exporté
vers d'autres modules).
Middleware
Fonctions exécutées avant le routing de Nest, avec un accès direct à
req, res et next() — le même modèle qu'Express. Utile
pour le logging brut, le parsing de cookies, ou des headers personnalisés appliqués
globalement.
Guard
Décident si une requête peut continuer, en renvoyant true/
false (ou en levant une exception). C'est l'endroit correct pour
l'authentification et l'autorisation — jamais dans le controller ou le service.
Interceptor
Se positionnent autour de l'exécution du handler de la route (comme un middleware AOP) : ils peuvent transformer la requête avant qu'elle n'atteigne le controller et la réponse avant qu'elle ne sorte. Cas d'usage typiques : logging des temps de réponse, transformation uniforme des réponses, caching, gestion des timeouts.
Pipe
Transforment et valident les données entrantes (paramètres de route, query string, body)
avant qu'elles n'atteignent le controller. ValidationPipe, intégré avec
class-validator, est de loin le pipe le plus utilisé dans toute API NestJS
sérieuse.
Exception Filter
Interceptent les exceptions levées n'importe où dans le cycle de la requête et les transforment en une réponse HTTP cohérente (status code, corps JSON structuré), évitant que des stack traces ou des erreurs brutes n'atteignent le client.
Installation
# 1. Crée un nouveau projet NestJS
nest new blog-api
# Pendant la création, choisis npm comme package manager quand demandé
cd blog-api
# 2. Installe les dépendances pour la base de données (TypeORM + driver PostgreSQL)
npm install @nestjs/typeorm typeorm pg
# 3. Installe les dépendances pour la validation
npm install class-validator class-transformer
# 4. Installe les dépendances pour l'authentification JWT
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm install --save-dev @types/passport-jwt @types/bcrypt
# 5. Installe les dépendances pour la sécurité et le rate limiting
npm install helmet @nestjs/throttler
# 6. Installe les dépendances pour l'upload de fichiers
npm install @nestjs/platform-express multer
npm install --save-dev @types/multer
# 7. Installe Swagger pour la documentation automatique de l'API
npm install @nestjs/swagger
# 8. Installe le logger structuré
npm install nestjs-pino pino-http pino-pretty
# 9. Configure les variables d'environnement
npm install @nestjs/config
Chaque commande installe un bloc fonctionnel bien précis : @nestjs/typeorm +
typeorm + pg connectent Nest à PostgreSQL via l'ORM TypeORM ;
class-validator/class-transformer activent des DTO validés
automatiquement ; la stack passport/passport-jwt/bcrypt
construit tout le flux d'authentification ; helmet et
@nestjs/throttler renforcent la sécurité HTTP et le rate limiting ;
multer gère le multipart/form-data pour les uploads ; nestjs-pino
fournit un logging JSON structuré, adapté à la production.
Implémentation Étape par Étape
1. Structure des Dossiers
src/
├── main.ts
├── app.module.ts
├── config/
│ └── configuration.ts
├── common/
│ ├── filters/
│ │ └── http-exception.filter.ts
│ ├── interceptors/
│ │ ├── logging.interceptor.ts
│ │ └── transform.interceptor.ts
│ ├── guards/
│ │ └── roles.guard.ts
│ ├── decorators/
│ │ └── roles.decorator.ts
│ └── pipes/
│ └── parse-object-id.pipe.ts
├── auth/
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ ├── strategies/
│ │ ├── jwt.strategy.ts
│ │ └── jwt-refresh.strategy.ts
│ └── dto/
│ ├── login.dto.ts
│ └── register.dto.ts
├── users/
│ ├── users.module.ts
│ ├── users.service.ts
│ └── entities/
│ └── user.entity.ts
└── posts/
├── posts.module.ts
├── posts.controller.ts
├── posts.service.ts
├── entities/
│ └── post.entity.ts
└── dto/
├── create-post.dto.ts
└── update-post.dto.ts
2. Configuration Centralisée avec @nestjs/config
// src/config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT ?? '3000', 10),
database: {
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',
name: process.env.DB_NAME ?? 'blog_db',
},
jwt: {
accessSecret: process.env.JWT_ACCESS_SECRET ?? 'change-me-access',
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES_IN ?? '15m',
refreshSecret: process.env.JWT_REFRESH_SECRET ?? 'change-me-refresh',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '7d',
},
});
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
import configuration from './config/configuration';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
type: 'postgres',
host: cfg.get('database.host'),
port: cfg.get('database.port'),
username: cfg.get('database.username'),
password: cfg.get('database.password'),
database: cfg.get('database.name'),
autoLoadEntities: true,
synchronize: process.env.NODE_ENV !== 'production', // ⚠️ développement uniquement
}),
}),
ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),
AuthModule,
UsersModule,
PostsModule,
],
})
export class AppModule {}
Ligne par ligne : ConfigModule.forRoot({ isGlobal: true })
rend ConfigService disponible dans toute l'application sans avoir à réimporter
le module partout. TypeOrmModule.forRootAsync construit la connexion à la base
de données de manière asynchrone, en lisant les valeurs depuis ConfigService au
lieu d'un objet statique — nécessaire pour pouvoir utiliser des variables d'environnement.
synchronize: true fait que TypeORM crée/met à jour automatiquement les tables
selon les entities : très pratique en développement, dangereux en production
(peut supprimer des données), où l'on utilisera plutôt des migrations.
3. Entities avec TypeORM
// src/users/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, OneToMany } from 'typeorm';
import { Post } from '../../posts/entities/post.entity';
import { Exclude } from 'class-transformer';
export enum UserRole {
ADMIN = 'admin',
AUTHOR = 'author',
READER = 'reader',
}
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
@Exclude() // exclut le hash du mot de passe des réponses sérialisées
passwordHash: string;
@Column({ type: 'enum', enum: UserRole, default: UserRole.READER })
role: UserRole;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
}
// src/posts/entities/post.entity.ts
import {
Entity, Column, PrimaryGeneratedColumn, ManyToOne,
CreateDateColumn, UpdateDateColumn, Index,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Index({ unique: true })
@Column()
slug: string;
@Column('text')
content: string;
@Column({ default: false })
published: boolean;
@Column({ nullable: true })
coverImageUrl?: string;
@ManyToOne(() => User, (user) => user.posts, { eager: true })
author: User;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
4. DTO avec class-validator
// src/posts/dto/create-post.dto.ts
import { IsString, IsBoolean, IsOptional, MinLength, MaxLength } from 'class-validator';
export class CreatePostDto {
@IsString()
@MinLength(5)
@MaxLength(200)
title: string;
@IsString()
@MinLength(20)
content: string;
@IsBoolean()
@IsOptional()
published?: boolean;
}
// src/posts/dto/update-post.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';
export class UpdatePostDto extends PartialType(CreatePostDto) {}
PartialType génère automatiquement une version où tous les champs du DTO
original deviennent optionnels — parfait pour les mises à jour partielles
(PATCH), sans dupliquer les décorateurs de validation.
5. Activer ValidationPipe Globalement
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet());
app.enableCors({ origin: process.env.CORS_ORIGIN?.split(',') ?? '*' });
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // supprime les propriétés absentes du DTO
forbidNonWhitelisted: true, // lève une erreur si des propriétés en trop arrivent
transform: true, // convertit automatiquement les types (ex. string → number)
}),
);
app.useGlobalFilters(new HttpExceptionFilter());
app.setGlobalPrefix('api/v1');
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
whitelist: true + forbidNonWhitelisted: true forment la paire la
plus importante pour la sécurité des DTO : sans cela, un client pourrait envoyer des champs
supplémentaires (ex. role: 'admin' dans une requête d'inscription) que TypeORM
pourrait persister par inadvertance si le code ne les filtre pas explicitement ailleurs.
6. Middleware Personnalisé
// src/common/middleware/request-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
req['requestId'] = randomUUID();
res.setHeader('X-Request-Id', req['requestId']);
next();
}
}
// enregistrement dans app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestIdMiddleware).forRoutes('*');
}
}
7. Guards : JwtAuthGuard et RolesGuard
// src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.get('jwt.accessSecret'),
});
}
async validate(payload: { sub: string; email: string; role: string }) {
// La valeur retournée est attachée à req.user
return { userId: payload.sub, email: payload.email, role: payload.role };
}
}
// src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '../../users/entities/user.entity';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles);
// src/common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { UserRole } from '../../users/entities/user.entity';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true; // aucune restriction de rôle sur cette route
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user?.role);
}
}
Le pattern Reflector.getAllAndOverride lit les métadonnées définies par le
décorateur @Roles(...) à la fois au niveau d'une seule méthode et de toute une
classe, permettant de définir une restriction de rôle sur un controller entier et de la
surcharger sur des routes individuelles si nécessaire.
8. Interceptors : Logging et Transformation de la Réponse
// src/common/interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable {
const req = context.switchToHttp().getRequest();
const start = Date.now();
return next.handle().pipe(
tap(() => {
const ms = Date.now() - start;
this.logger.log(`${req.method} ${req.url} — ${ms}ms`);
}),
);
}
}
// src/common/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response {
success: true;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor implements NestInterceptor> {
intercept(context: ExecutionContext, next: CallHandler): Observable> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
9. Exception Filter Global
// src/common/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.getResponse()
: 'Errore interno del server';
// Journalise la stack trace complète uniquement côté serveur, jamais dans la réponse au client
if (!(exception instanceof HttpException)) {
this.logger.error(exception instanceof Error ? exception.stack : exception);
}
response.status(status).json({
success: false,
statusCode: status,
path: request.url,
timestamp: new Date().toISOString(),
message,
});
}
}
Un détail de sécurité crucial : lorsque l'exception n'est pas une
HttpException connue (donc une erreur inattendue, ex. un bug ou une erreur de
base de données), le message renvoyé au client est générique ("Erreur interne du serveur")
— la vraie stack trace n'est journalisée que côté serveur. Exposer des stack traces aux
clients est un problème de sécurité connu (information disclosure).
10. Pipe Personnalisé
// src/common/pipes/parse-uuid-or-404.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, NotFoundException } from '@nestjs/common';
import { isUUID } from 'class-validator';
@Injectable()
export class ParseUuidOr404Pipe implements PipeTransform {
transform(value: string, _metadata: ArgumentMetadata): string {
if (!isUUID(value)) {
throw new NotFoundException(`Risorsa con id "${value}" non trovata`);
}
return value;
}
}
Renvoyer un 404 plutôt qu'un 400 Bad Request générique quand l'id
n'est même pas un UUID valide est un choix délibéré : du point de vue du client, "ressource
introuvable" est sémantiquement plus correct et ne révèle pas de détails sur le format
interne des ids.
Exemple Réel : une API de Blog Complète
Assemblons toutes les pièces dans un module PostsModule complet, avec CRUD,
authentification, autorisation basée sur les rôles et upload de l'image de couverture.
Auth Service : login, register, refresh token
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { UsersService } from '../users/users.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private config: ConfigService,
) {}
async register(dto: RegisterDto) {
const existing = await this.usersService.findByEmail(dto.email);
if (existing) throw new ConflictException('Email già registrata');
const passwordHash = await bcrypt.hash(dto.password, 12);
const user = await this.usersService.create({ ...dto, passwordHash });
return this.buildTokens(user.id, user.email, user.role);
}
async login(dto: LoginDto) {
const user = await this.usersService.findByEmail(dto.email);
if (!user) throw new UnauthorizedException('Credenziali non valide');
const passwordValid = await bcrypt.compare(dto.password, user.passwordHash);
if (!passwordValid) throw new UnauthorizedException('Credenziali non valide');
return this.buildTokens(user.id, user.email, user.role);
}
async refresh(userId: string, email: string, role: string) {
// En production : vérifie aussi que le refresh token est toujours valide côté serveur
// (whitelist/blacklist) pour pouvoir le révoquer, ex. lors du logout.
return this.buildTokens(userId, email, role);
}
private buildTokens(sub: string, email: string, role: string) {
const payload = { sub, email, role };
const accessToken = this.jwtService.sign(payload, {
secret: this.config.get('jwt.accessSecret'),
expiresIn: this.config.get('jwt.accessExpiresIn'),
});
const refreshToken = this.jwtService.sign(payload, {
secret: this.config.get('jwt.refreshSecret'),
expiresIn: this.config.get('jwt.refreshExpiresIn'),
});
return { accessToken, refreshToken };
}
}
Pourquoi deux tokens séparés ? L'access token a une durée de vie courte (15 minutes) et c'est celui envoyé à chaque requête — s'il est volé, le dommage est limité dans le temps. Le refresh token a une durée de vie plus longue (jours) mais n'est utilisé que pour obtenir un nouvel access token sur un endpoint dédié, réduisant la surface d'attaque.
Auth Controller
// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
@UseGuards(AuthGuard('jwt-refresh'))
@Post('refresh')
refresh(@Req() req) {
return this.authService.refresh(req.user.userId, req.user.email, req.user.role);
}
}
Posts Controller : CRUD, RBAC et Upload
// src/posts/posts.controller.ts
import {
Controller, Get, Post, Body, Patch, Param, Delete,
UseGuards, UseInterceptors, UploadedFile, Query,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { AuthGuard } from '@nestjs/passport';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';
import { UserRole } from '../users/entities/user.entity';
import { ParseUuidOr404Pipe } from '../common/pipes/parse-uuid-or-404.pipe';
import { CurrentUser } from '../common/decorators/current-user.decorator';
@Controller('posts')
export class PostsController {
constructor(private postsService: PostsService) {}
@Get()
findAll(@Query('page') page = 1, @Query('limit') limit = 10) {
return this.postsService.findAll(Number(page), Number(limit));
}
@Get(':id')
findOne(@Param('id', ParseUuidOr404Pipe) id: string) {
return this.postsService.findOne(id);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.AUTHOR, UserRole.ADMIN)
@Post()
create(@Body() dto: CreatePostDto, @CurrentUser() user) {
return this.postsService.create(dto, user.userId);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.AUTHOR, UserRole.ADMIN)
@Patch(':id')
update(@Param('id', ParseUuidOr404Pipe) id: string, @Body() dto: UpdatePostDto, @CurrentUser() user) {
return this.postsService.update(id, dto, user.userId, user.role);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles(UserRole.ADMIN)
@Delete(':id')
remove(@Param('id', ParseUuidOr404Pipe) id: string) {
return this.postsService.remove(id);
}
@UseGuards(AuthGuard('jwt'))
@Post(':id/cover')
@UseInterceptors(FileInterceptor('file', {
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (_req, file, cb) => {
const allowed = ['image/jpeg', 'image/png', 'image/webp'];
cb(null, allowed.includes(file.mimetype));
},
}))
uploadCover(@Param('id', ParseUuidOr404Pipe) id: string, @UploadedFile() file: Express.Multer.File) {
return this.postsService.setCoverImage(id, file);
}
}
Remarque comme chaque route applique seulement les guards et rôles
strictement nécessaires : findAll/findOne sont publiques (aucun
guard), create/update exigent le rôle author ou
admin, tandis que remove est réservée au seul admin
— un exemple concret du principe du moindre privilège appliqué route par
route.
Posts Service
// src/posts/posts.service.ts
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './entities/post.entity';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { UserRole } from '../users/entities/user.entity';
import slugify from 'slugify';
@Injectable()
export class PostsService {
constructor(@InjectRepository(Post) private postsRepo: Repository) {}
async findAll(page: number, limit: number) {
const [items, total] = await this.postsRepo.findAndCount({
where: { published: true },
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
return { items, total, page, limit };
}
async findOne(id: string): Promise {
const post = await this.postsRepo.findOne({ where: { id } });
if (!post) throw new NotFoundException(`Post ${id} non trovato`);
return post;
}
async create(dto: CreatePostDto, authorId: string): Promise {
const post = this.postsRepo.create({
...dto,
slug: slugify(dto.title, { lower: true, strict: true }),
author: { id: authorId } as any,
});
return this.postsRepo.save(post);
}
async update(id: string, dto: UpdatePostDto, userId: string, userRole: UserRole): Promise {
const post = await this.findOne(id);
// Un author ne peut modifier que ses propres posts ; l'admin peut tous les modifier
if (userRole !== UserRole.ADMIN && post.author.id !== userId) {
throw new ForbiddenException('Non puoi modificare un post di un altro autore');
}
Object.assign(post, dto);
return this.postsRepo.save(post);
}
async remove(id: string): Promise {
const result = await this.postsRepo.delete(id);
if (result.affected === 0) throw new NotFoundException(`Post ${id} non trovato`);
}
async setCoverImage(id: string, file: Express.Multer.File): Promise {
const post = await this.findOne(id);
// En production : upload vers S3/Cloud Storage au lieu du disque local
post.coverImageUrl = `/uploads/${file.filename}`;
return this.postsRepo.save(post);
}
}
Observe la vérification userRole !== UserRole.ADMIN && post.author.id !== userId
dans la méthode update : c'est un exemple d'autorisation au niveau de
la ressource (pas seulement de la route) — un author authentifié avec un token JWT
valide peut quand même être bloqué s'il tente de modifier la ressource de quelqu'un d'autre.
Cette vérification doit toujours être faite dans le service, jamais déléguée uniquement au
guard, qui opère à un niveau trop générique pour connaître le propriétaire d'une ressource
spécifique.
Bonnes Pratiques
| Principe | Application Pratique dans NestJS |
|---|---|
| Clean Architecture | Séparation nette entre controller (HTTP), service (logique métier) et repository (persistance). Le domaine ne doit jamais dépendre de détails d'infrastructure comme Express ou TypeORM. |
| SOLID — Single Responsibility | Un controller mappe des requêtes HTTP, un service contient une seule zone de logique métier, un repository un seul aggregate. |
| SOLID — Dependency Inversion | Les services dépendent d'interfaces/tokens abstraits (ex. @InjectRepository), pas d'implémentations concrètes — remplaçables par des mocks dans les tests. |
| DRY | Logique de validation partagée dans les DTO avec PartialType/PickType/OmitType, évitant la duplication entre DTO de Create et Update. |
| KISS | Évite d'introduire CQRS, Event Sourcing ou des microservices tant que la complexité réelle du domaine ne le justifie pas. |
| Repository Pattern | TypeORM/Prisma fournissent déjà cette couche ; évite de "percer" l'abstraction en appelant des requêtes SQL brutes directement dans les services, sauf cas critiques de performance. |
| DTO | Chaque endpoint a un DTO d'entrée dédié — n'expose jamais directement les entities de la base de données comme contrat d'API. |
| Validation | ValidationPipe global avec whitelist et forbidNonWhitelisted toujours actifs dans chaque projet, sans exception. |
Performance
Caching
// Caching au niveau de l'endpoint avec @nestjs/cache-manager
import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';
import { UseInterceptors } from '@nestjs/common';
@UseInterceptors(CacheInterceptor)
@CacheTTL(60) // cache pendant 60 secondes
@Get()
findAll() {
return this.postsService.findAll(1, 10);
}
Lazy Loading des Modules
Dans les grandes applications monolithiques, LazyModuleLoader permet de charger
des modules peu utilisés (ex. un module de reporting) seulement lorsqu'ils sont réellement
nécessaires, réduisant le temps de bootstrap.
Requêtes Optimisées
- Utilise un
selectexplicite dans TypeORM pour éviter de transférer des colonnes inutiles (ex. exclurecontentdans les listes). - Ajoute des index sur les colonnes fréquemment utilisées dans
WHERE/ORDER BY(voir le@Indexsur le slug dans l'entityPost). - Évite le problème N+1 : utilise
relationsouQueryBuilderavecleftJoinAndSelectau lieu de charger les relations dans une boucle. - Utilise toujours la pagination (
skip/take) — ne renvoie jamais de collections illimitées.
Logging et Monitoring
En production, remplace le logger par défaut par nestjs-pino pour des logs JSON
structurés, facilement indexables par des outils comme Grafana Loki ou Datadog. Pour le
monitoring de performance, intègre @nestjs/terminus pour les health checks
(/health) utilisés par les load balancers et orchestrateurs (Kubernetes,
Railway).
Profiling
Pour trouver de vrais goulets d'étranglement, utilise node --prof ou des outils
APM (Application Performance Monitoring) comme New Relic ou Elastic APM, qui tracent le
temps passé dans chaque handler, requête et appel externe — évite d'optimiser "au feeling"
sans données réelles.
Sécurité
| Mesure | Comment l'Implémenter |
|---|---|
| JWT | Access token à durée de vie courte signé avec un secret robuste (32+ caractères aléatoires), jamais hardcodé dans le code. |
| Refresh Token | Durée de vie plus longue, idéalement révocable (whitelist dans Redis/DB), rotation à chaque utilisation. |
| Hachage du Mot de Passe / bcrypt | bcrypt.hash(password, 12) — jamais MD5/SHA1, jamais de mots de passe en clair en base. |
| Helmet | app.use(helmet()) définit les headers de sécurité (CSP, X-Frame-Options, HSTS) en une seule ligne. |
| CORS | app.enableCors({ origin: [...] }) avec une whitelist explicite de domaines, jamais origin: '*' en production si l'API nécessite des credentials. |
| Rate Limiting | @nestjs/throttler pour limiter les requêtes par IP, essentiel sur des endpoints comme /auth/login pour atténuer les attaques par force brute. |
| ValidationPipe | whitelist + forbidNonWhitelisted pour bloquer le mass assignment sur des champs imprévus. |
| Sanitisation de l'Input | class-validator pour la structure, plus une sanitisation HTML explicite (ex. sanitize-html) si un champ autorise du markup libre, pour prévenir le XSS. |
// Exemple : rate limiting plus agressif spécifiquement sur le login
import { Throttle } from '@nestjs/throttler';
@Throttle({ default: { limit: 5, ttl: 60000 } }) // max 5 tentatives par minute et par IP
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
Erreurs Courantes
1. Oublier whitelist/forbidNonWhitelisted dans ValidationPipe
Problème : un client peut envoyer des champs supplémentaires non prévus par le DTO (ex. role: 'admin').
Cause : ValidationPipe configuré sans whitelist: true.
Solution : active toujours whitelist et forbidNonWhitelisted globalement dans main.ts.
2. synchronize: true en Production
Problème : TypeORM peut altérer/supprimer des colonnes ou des tables à l'exécution.
Cause : confusion entre l'environnement de développement et de production dans la configuration de la base de données.
Solution : synchronize: false en production, gère le schéma avec des migrations explicites (typeorm migration:generate).
3. Dépendance Circulaire Entre Modules
Problème : erreur "Nest cannot resolve dependencies" ou crash silencieux au démarrage.
Cause : deux modules s'importent mutuellement de façon directe.
Solution : utilise forwardRef(() => ModuleB) dans les deux modules, ou — mieux — extrait la dépendance partagée dans un troisième module.
4. Exposer les Entities Directement comme Réponse d'API
Problème : des champs sensibles (ex. passwordHash) finissent dans la réponse JSON.
Cause : le controller renvoie l'entity TypeORM telle quelle, sans couche DTO/serializer.
Solution : utilise @Exclude() de class-transformer sur l'entity avec un ClassSerializerInterceptor global, ou mieux encore un DTO de réponse explicite.
5. Mots de Passe Stockés Sans Hachage
Problème : une compromission de la base expose tous les mots de passe en clair.
Cause : omission (souvent pendant le prototypage rapide) de l'étape de hachage.
Solution : hachage avec bcrypt/argon2 toujours, même en développement — il n'y a jamais de raison valable de le sauter.
6. Secrets JWT Faibles ou Hardcodés dans le Code
Problème : un secret prévisible permet de falsifier des tokens valides.
Cause : un secret par défaut laissé en production, ou commité dans le dépôt.
Solution : des secrets générés aléatoirement (au moins 256 bits), gérés uniquement via des variables d'environnement/un secret manager, jamais dans le code source.
7. Absence de Gestion des Erreurs Asynchrones dans les Services
Problème : des promise rejections non gérées provoquent un crash du processus Node.
Cause : des appels async sans try/catch là où c'est nécessaire, ou l'oubli de l'exception filter global.
Solution : laisse les exceptions remonter jusqu'à l'exception filter global (comportement par défaut de Nest), n'intercepte que là où un comportement différent est nécessaire (fallback, retry).
8. Requêtes N+1 Non Détectées
Problème : l'endpoint devient extrêmement lent à mesure que les données augmentent.
Cause : chargement des relations dans une boucle au lieu d'un seul join.
Solution : utilise relations/leftJoinAndSelect, surveille les requêtes générées avec logging: true en développement.
9. CORS Ouvert à Tous en Production avec Credentials
Problème : n'importe quel site web peut effectuer des requêtes authentifiées vers l'API au nom de l'utilisateur.
Cause : origin: '*' combiné avec credentials: true.
Solution : whitelist explicite des domaines autorisés.
10. Aucun Rate Limiting sur les Endpoints d'Authentification
Problème : attaques par force brute sur le login/l'inscription.
Cause : @nestjs/throttler non appliqué, ou avec des seuils trop permissifs.
Solution : un @Throttle plus restrictif spécifiquement sur login/register/reset de mot de passe.
11. DTO d'Update Identique au DTO de Create Sans PartialType
Problème : chaque PATCH exige tous les champs obligatoires, cassant les mises à jour partielles.
Cause : le DTO de Create est copié sans rendre les champs optionnels.
Solution : class UpdateXDto extends PartialType(CreateXDto) {}.
12. Guards Appliqués Uniquement au Niveau de la Route, Jamais de la Ressource
Problème : un utilisateur authentifié peut modifier des ressources d'autres utilisateurs.
Cause : on se fie uniquement à @UseGuards(AuthGuard('jwt')) sans vérification d'ownership dans le service.
Solution : vérification explicite resource.ownerId === user.id (ou rôle admin) dans le service, comme montré dans l'exemple PostsService.update.
13. Connexion à la Base de Données Sans Pool Correctement Configuré
Problème : épuisement des connexions sous charge, erreurs "too many clients".
Cause : valeurs par défaut du pool non adaptées au trafic réel.
Solution : configure explicitement extra: { max: 20 } dans la connection de TypeORM, calibré selon le plan de la base de données.
14. Upload de Fichiers Sans Limites de Taille ou de Type
Problème : un utilisateur malveillant upload des fichiers énormes ou des exécutables déguisés en images.
Cause : FileInterceptor configuré sans limits ni fileFilter.
Solution : définis toujours limits.fileSize et valide le mimetype, comme dans l'exemple uploadCover.
15. Variables d'Environnement Non Validées au Démarrage
Problème : l'app démarre "silencieusement" avec une configuration incomplète et échoue de manière cryptique plus tard.
Cause : aucun schéma de validation sur les env vars.
Solution : utilise ConfigModule.forRoot({ validationSchema: Joi.object({...}) }) pour faire échouer le bootstrap immédiatement si une variable critique manque.
16. Logging de Données Sensibles
Problème : mots de passe, tokens ou données personnelles finissent dans les logs en clair.
Cause : logging indiscriminé de tout le body de la requête.
Solution : rédige (masque) explicitement les champs sensibles avant le logging, ou utilise les redact paths de pino.
17. Tests Écrits Uniquement pour le "Happy Path"
Problème : des bugs qui n'apparaissent que sur des entrées invalides ou des cas limites restent invisibles jusqu'en production.
Cause : pression temporelle, couverture de tests limitée au flux principal.
Solution : teste explicitement les erreurs 4xx attendues (validation, permissions, not found), pas seulement les 2xx.
18. Absence de Versionnement de l'API
Problème : chaque changement breaking casse les clients existants sans préavis.
Cause : aucun préfixe de version (/api/v1) ou stratégie de versionnement Nest configurée.
Solution : app.setGlobalPrefix('api/v1') dès le premier jour, même sur de petits projets.
19. Dépendances Obsolètes avec des Vulnérabilités Connues
Problème : l'API reste exposée à des CVE publiquement documentées.
Cause : absence d'un processus d'audit périodique des dépendances.
Solution : npm audit intégré à la CI, mises à jour régulières avec Dependabot/Renovate.
20. Aucune Distinction Entre Erreurs de Validation et Erreurs de Domaine
Problème : toutes les erreurs reviennent sous forme de 500 générique, rendant impossible pour le client de distinguer une entrée erronée d'un problème serveur.
Cause : utilisation d'Error générique au lieu des classes HttpException spécifiques de Nest.
Solution : utilise toujours les exceptions typées correctes (BadRequestException, NotFoundException, ForbiddenException, ConflictException...) — c'est ainsi que Nest communique le bon status code HTTP.
FAQ
1. NestJS convient-il aussi aux petits projets ?
Oui, mais la vraie valeur apparaît quand le projet grandit : sur un petit script, la structure peut sembler excessive comparée à Express pur.
2. Dois-je obligatoirement utiliser TypeScript ?
Techniquement, NestJS supporte aussi le JavaScript pur, mais on perd une grande partie de la valeur (décorateurs typés, DI type-safe). C'est fortement déconseillé en production.
3. TypeORM ou Prisma ?
TypeORM s'intègre plus nativement avec les décorateurs NestJS et est plus mature dans l'écosystème Nest ; Prisma offre un client généré plus type-safe et une meilleure developer experience pour les migrations, mais nécessite une couche d'intégration manuelle avec Nest.
4. Comment gérer les migrations de base de données ?
Avec TypeORM : typeorm migration:generate pour les générer à partir du diff des entities, typeorm migration:run pour les appliquer en CI/CD, jamais synchronize: true en production.
5. Comment structurer un très grand projet ?
Modularise par domaine (feature modules), pas par type technique — évite les dossiers globaux "controllers/", "services/" qui mélangent des domaines différents.
6. Comment tester les controllers ?
Avec Test.createTestingModule de @nestjs/testing, en mockant les services injectés ; pour les services, mock les repositories TypeORM avec getRepositoryToken.
7. Quelle est la différence entre Guard et Middleware pour l'authentification ?
Le middleware n'a pas accès au contexte d'exécution de Nest (ex. métadonnées des décorateurs), il ne peut donc pas lire @Roles(). Les guards oui — c'est pourquoi l'authentification/autorisation va toujours dans les guards, jamais dans les middlewares.
8. Puis-je utiliser GraphQL au lieu de REST ?
Oui, NestJS a un support officiel pour Apollo Server comme pour Mercurius, avec le même système de modules/DI — utile si tu as besoin de requêtes flexibles côté client.
9. Comment gérer les transactions de base de données ?
Avec DataSource.transaction() de TypeORM, ou avec le décorateur @Transactional() de la librairie typeorm-transactional pour une syntaxe plus déclarative.
10. Comment implémenter le refresh token de manière sécurisée ?
Stocke un hash du refresh token côté serveur (whitelist), fais-le tourner à chaque utilisation (token rotation), et invalide-le explicitement à la déconnexion.
11. NestJS supporte-t-il les microservices ?
Oui, nativement, avec des transport layers pour TCP, Redis, RabbitMQ, Kafka, gRPC et NATS via @nestjs/microservices.
12. Comment déployer en production ?
Build avec nest build (compile dans dist/), puis node dist/main.js ; sur des plateformes comme Railway/Render/Fly.io, un Dockerfile multi-stage ou le buildpack Node natif suffit.
13. Comment gérer différentes variables d'environnement pour dev/staging/prod ?
ConfigModule.forRoot({ envFilePath: ['.env.${NODE_ENV}', '.env'] }), avec les vraies valeurs injectées par la plateforme d'hébergement en production, pas depuis des fichiers .env commités.
14. Comment implémenter l'upload vers S3 au lieu du disque local ?
Remplace le storage engine de Multer par multer-s3, ou gère l'upload manuellement dans le service avec le SDK AWS après avoir reçu le buffer en mémoire.
15. Comment documenter automatiquement l'API ?
Avec @nestjs/swagger : les décorateurs @ApiTags, @ApiProperty sur les DTO, et SwaggerModule.setup() dans main.ts génèrent une UI OpenAPI interactive sur /api/docs.
16. Comment implémenter le soft delete ?
TypeORM supporte nativement @DeleteDateColumn() sur l'entity : softDelete() définit la colonne au lieu de supprimer la ligne, et les requêtes excluent automatiquement les enregistrements supprimés.
17. Comment gérer plusieurs versions de l'API en même temps ?
Avec app.enableVersioning({ type: VersioningType.URI }) et le décorateur @Version('2') sur les controllers/méthodes individuels devant coexister avec la v1.
18. Un ORM est-il nécessaire, ou puis-je utiliser des requêtes SQL directes ?
Pour la plupart des cas, un ORM réduit les erreurs et le boilerplate ; pour des requêtes très complexes ou critiques en performance, TypeORM permet quand même des requêtes brutes via QueryRunner tout en gardant le reste de l'app sur l'ORM.
19. Comment implémenter WebSocket dans NestJS ?
Avec @nestjs/websockets et un @WebSocketGateway(), qui s'intègre avec Socket.IO ou ws tout en gardant le même système de DI et de guards que les routes REST.
20. Comment gérer les cron jobs ?
Avec @nestjs/schedule et le décorateur @Cron('0 0 * * *') sur une méthode d'un service — utile pour le nettoyage de données, l'envoi de digests par email, les synchronisations périodiques.
21. Comment protéger l'API contre les attaques par SQL Injection ?
TypeORM/Prisma paramètrent automatiquement les requêtes — le risque n'apparaît que si tu construis des requêtes brutes en concaténant des chaînes manuellement, ce qui doit toujours être évité.
22. Quelle est la bonne façon de gérer les secrets en production ?
Jamais dans le code ou dans des fichiers commités : utilise les variables d'environnement de la plateforme d'hébergement ou un secret manager dédié (AWS Secrets Manager, Doppler, Railway Variables).
23. Comment scaler horizontalement une API NestJS ?
L'app doit être stateless (aucune session locale en mémoire — utilise JWT ou Redis pour l'état partagé), puis elle est répliquée derrière un load balancer ; pour WebSocket, un adapter Redis est nécessaire pour synchroniser les connexions entre instances.
24. Comment gérer la rétrocompatibilité quand je change un schéma de réponse ?
Introduis un nouveau champ plutôt que de renommer l'existant, déprécie-le progressivement avec une documentation claire, et utilise le versionnement de l'API pour les changements réellement breaking.
25. Vaut-il la peine d'écrire des tests E2E en plus des unit tests pour une API NestJS ?
Oui — @nestjs/testing avec supertest permet de tester toute la pipeline (guards, pipes, interceptors inclus) en appelant les endpoints réels sur une instance de l'app en mémoire, capturant des problèmes d'intégration que les unit tests seuls ne voient pas.
Ressources et Documentation Officielle
Pour approfondir chaque sujet traité dans ce guide, la documentation officielle reste toujours la source la plus fiable et à jour :
- NestJS — documentation officielle :
docs.nestjs.com, en particulier les sections sur les Guards, Interceptors, Pipes et Exception Filters. - TypeORM — documentation officielle :
typeorm.io, pour les relations, les migrations et le query builder avancé. - Passport.js :
passportjs.org, la librairie d'authentification sur laquelle repose@nestjs/passport. - class-validator : dépôt officiel sur GitHub, avec la liste complète des décorateurs de validation disponibles au-delà de ceux utilisés dans ce guide.
- OWASP API Security Top 10 : la checklist de référence pour la sécurité des API REST, complémentaire à la section Sécurité de ce guide.
- jwt.io : outil pour inspecter et décoder les tokens JWT lors du débogage de l'authentification.
Articles Connexes Recommandés
Ce guide couvre tout le cycle de vie d'une REST API NestJS, mais chaque section peut devenir un approfondissement à part entière. Voici les sujets naturellement liés qui méritent d'être explorés ensuite :
- Modulariser un grand projet NestJS : feature modules, shared modules et barrel files.
- Dependency Injection dans NestJS expliquée avec des exemples : providers personnalisés, factory providers, tokens d'injection.
- Authentification JWT et Refresh Token en profondeur : token rotation, révocation et gestion multi-appareils.
- Role Based Access Control (RBAC) avancé : permissions granulaires au-delà des simples rôles.
- Validation avec ValidationPipe et class-validator : validateurs personnalisés et messages d'erreur localisés.
- Logging centralisé dans NestJS avec Pino et corrélation des requêtes via request ID.
- Exception Filters personnalisés pour des domaines spécifiques (ex. erreurs de paiement, erreurs de domaine).
- Upload de fichiers avancé avec Multer : stockage sur S3, validation du contenu réel des fichiers.
- Scheduler et Cron Jobs dans NestJS avec
@nestjs/schedule. - WebSocket avec NestJS : notifications en temps réel et scaling avec l'adapter Redis.
- Testing des controllers et services NestJS : unit tests, mock des repositories, tests E2E avec Supertest.
Conclusion
Construire une REST API avec NestJS signifie investir dans une structure qui évolue avec la complexité du projet : des modules bien isolés, Dependency Injection pour la testabilité, guards/interceptors/pipes pour séparer les cross-cutting concerns de la logique métier, et un écosystème — TypeORM, Passport, class-validator, Swagger — qui couvre de manière mature chaque besoin réel de production. L'exemple d'API de Blog construit dans ce guide (CRUD, JWT avec refresh token, RBAC, upload de fichiers, logging centralisé, gestion des erreurs) est le squelette réutilisable pour la plupart des API NestJS que tu rencontreras en pratique.
Prochaines étapes recommandées : approfondis la modularisation des grands projets NestJS, l'authentification JWT et le refresh token en détail, le pattern RBAC avec permissions granulaires, et les techniques de testing pour controllers et services — des sujets qui complètent naturellement ce guide.