NestJS : Créer une API REST complète
Guide complet pour créer une API REST professionnelle avec NestJS. Controllers, Services, Modules, validation avec class-validator et gestion des erreurs expliqués.

NestJS s'impose comme le framework Node.js de référence pour créer des applications serveur scalables et maintenables. Inspiré par Angular, il apporte une architecture modulaire, l'injection de dépendances et TypeScript natif. Construire une API REST professionnelle devient structuré et prévisible.
NestJS 11 introduit des améliorations de performance significatives, un support natif des signaux pour l'arrêt gracieux, et une intégration simplifiée avec les ORMs modernes comme Prisma et Drizzle. Le framework reste backward-compatible avec les versions précédentes.
Installation et configuration du projet
La CLI NestJS génère un projet complet avec une structure prête pour la production. TypeScript, ESLint et les tests unitaires sont configurés automatiquement.
# terminal
# Installation globale de la CLI NestJS
npm install -g @nestjs/cli
# Création d'un nouveau projet
nest new my-api
# Navigation dans le projet
cd my-api
# Démarrage en mode développement avec hot-reload
npm run start:devCette commande crée un projet avec une structure de fichiers organisée et les dépendances essentielles pré-installées.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
// Création de l'application NestJS
const app = await NestFactory.create(AppModule);
// Activation de la validation globale
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Supprime les propriétés non décorées
forbidNonWhitelisted: true, // Rejette les requêtes avec propriétés inconnues
transform: true, // Transforme les payloads en instances de DTO
}));
// Configuration du préfixe global pour toutes les routes
app.setGlobalPrefix('api');
await app.listen(3000);
}
bootstrap();Cette configuration active la validation automatique des requêtes et préfixe toutes les routes avec /api.
Comprendre l'architecture modulaire
NestJS organise le code en modules, chacun encapsulant un domaine fonctionnel. Les modules déclarent les controllers (entrées HTTP), providers (services métier) et imports (dépendances).
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { ProductsModule } from './products/products.module';
import { AuthModule } from './auth/auth.module';
// Le module racine importe tous les modules de l'application
@Module({
imports: [
UsersModule, // Gestion des utilisateurs
ProductsModule, // Catalogue produits
AuthModule, // Authentification
],
})
export class AppModule {}Chaque module métier suit la même structure : un fichier module, un controller et un service.
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
// Controllers gèrent les requêtes HTTP
controllers: [UsersController],
// Providers sont injectables dans tout le module
providers: [UsersService],
// Exports rendent les providers disponibles pour d'autres modules
exports: [UsersService],
})
export class UsersModule {}L'export de UsersService permet aux autres modules d'importer UsersModule et d'utiliser ce service via injection de dépendances.
Création d'un Controller CRUD complet
Les controllers définissent les routes HTTP et délèguent la logique métier aux services. Les décorateurs de NestJS rendent le code expressif et auto-documenté.
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
ParseIntPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
// Préfixe de route : /api/users
@Controller('users')
export class UsersController {
// Injection du service via le constructeur
constructor(private readonly usersService: UsersService) {}
// POST /api/users - Création d'un utilisateur
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() createUserDto: CreateUserDto): Promise<User> {
// Le DTO est automatiquement validé avant d'arriver ici
return this.usersService.create(createUserDto);
}
// GET /api/users - Liste avec pagination
@Get()
async findAll(
@Query('page', new ParseIntPipe({ optional: true })) page: number = 1,
@Query('limit', new ParseIntPipe({ optional: true })) limit: number = 10,
): Promise<{ data: User[]; total: number }> {
return this.usersService.findAll(page, limit);
}
// GET /api/users/:id - Récupération par ID
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number): Promise<User> {
// ParseIntPipe convertit et valide automatiquement le paramètre
return this.usersService.findOne(id);
}
// PUT /api/users/:id - Mise à jour complète
@Put(':id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
): Promise<User> {
return this.usersService.update(id, updateUserDto);
}
// DELETE /api/users/:id - Suppression
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
await this.usersService.remove(id);
}
}Les pipes comme ParseIntPipe assurent la conversion et validation des paramètres. En cas d'échec, une erreur 400 est automatiquement renvoyée.
NestJS fournit plusieurs pipes intégrés : ParseIntPipe, ParseBoolPipe, ParseArrayPipe, ParseUUIDPipe. Chacun valide et transforme les données entrantes avant l'exécution du handler.
Implémentation du Service métier
Les services encapsulent la logique métier et les accès aux données. Marqués avec @Injectable(), ils sont gérés par le conteneur d'injection de dépendances de NestJS.
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
// Simulation d'une base de données en mémoire
private users: User[] = [];
private idCounter = 1;
async create(createUserDto: CreateUserDto): Promise<User> {
// Création de l'entité utilisateur
const user: User = {
id: this.idCounter++,
...createUserDto,
createdAt: new Date(),
updatedAt: new Date(),
};
this.users.push(user);
return user;
}
async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> {
// Calcul de la pagination
const start = (page - 1) * limit;
const end = start + limit;
return {
data: this.users.slice(start, end),
total: this.users.length,
};
}
async findOne(id: number): Promise<User> {
const user = this.users.find(u => u.id === id);
// Levée d'exception si l'utilisateur n'existe pas
if (!user) {
throw new NotFoundException(`Utilisateur avec l'ID ${id} introuvable`);
}
return user;
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id);
// Fusion des données existantes avec les mises à jour
Object.assign(user, updateUserDto, { updatedAt: new Date() });
return user;
}
async remove(id: number): Promise<void> {
const index = this.users.findIndex(u => u.id === id);
if (index === -1) {
throw new NotFoundException(`Utilisateur avec l'ID ${id} introuvable`);
}
this.users.splice(index, 1);
}
// Méthode utilitaire pour d'autres services
async findByEmail(email: string): Promise<User | undefined> {
return this.users.find(u => u.email === email);
}
}L'exception NotFoundException génère automatiquement une réponse HTTP 404 avec un message d'erreur formaté.
Prêt à réussir tes entretiens Node.js / NestJS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Validation des données avec class-validator
Les DTOs (Data Transfer Objects) définissent la structure des données entrantes. Les décorateurs de class-validator spécifient les règles de validation appliquées automatiquement.
import {
IsEmail,
IsNotEmpty,
IsString,
MinLength,
MaxLength,
IsOptional,
Matches,
} from 'class-validator';
export class CreateUserDto {
@IsNotEmpty({ message: "L'email est obligatoire" })
@IsEmail({}, { message: "Format d'email invalide" })
email: string;
@IsNotEmpty({ message: 'Le mot de passe est obligatoire' })
@MinLength(8, { message: 'Le mot de passe doit contenir au moins 8 caractères' })
@MaxLength(50, { message: 'Le mot de passe ne peut pas dépasser 50 caractères' })
@Matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
{ message: 'Le mot de passe doit contenir au moins une majuscule, une minuscule et un chiffre' }
)
password: string;
@IsNotEmpty({ message: 'Le prénom est obligatoire' })
@IsString()
@MinLength(2)
@MaxLength(50)
firstName: string;
@IsNotEmpty({ message: 'Le nom est obligatoire' })
@IsString()
@MinLength(2)
@MaxLength(50)
lastName: string;
@IsOptional()
@IsString()
@MaxLength(20)
phone?: string;
}Pour les mises à jour partielles, PartialType rend tous les champs optionnels tout en conservant les validations.
import { PartialType, OmitType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
// Tous les champs de CreateUserDto deviennent optionnels
// Le mot de passe est exclu des mises à jour standard
export class UpdateUserDto extends PartialType(
OmitType(CreateUserDto, ['password'] as const)
) {}L'entité représente la structure des données stockées.
export class User {
id: number;
email: string;
password: string;
firstName: string;
lastName: string;
phone?: string;
createdAt: Date;
updatedAt: Date;
}Gestion centralisée des erreurs
NestJS fournit des exceptions HTTP intégrées. Un filtre d'exception global permet de personnaliser le format des réponses d'erreur.
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
// Capture toutes les HttpException
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
// Récupération du message d'erreur (peut être string ou objet)
const exceptionResponse = exception.getResponse();
const message = typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message;
// Format de réponse standardisé
response.status(status).json({
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message: message,
});
}
}Pour capturer les erreurs non-HTTP (erreurs système, exceptions non gérées), un second filtre assure une gestion complète.
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
// Capture TOUTES les exceptions (y compris les erreurs système)
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
// Détermination du code HTTP et du message
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.message
: 'Erreur interne du serveur';
// Log de l'erreur pour le debugging
console.error('Exception caught:', exception);
response.status(status).json({
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: message,
});
}
}Enregistrement global des filtres dans le module principal.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Validation globale
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
// Filtres d'exceptions globaux
app.useGlobalFilters(
new AllExceptionsFilter(),
new HttpExceptionFilter(),
);
app.setGlobalPrefix('api');
await app.listen(3000);
}
bootstrap();L'ordre d'enregistrement des filtres est important. Le premier filtre enregistré est le dernier exécuté. AllExceptionsFilter doit être enregistré avant HttpExceptionFilter pour servir de fallback.
Intégration avec Prisma ORM
Prisma simplifie les interactions avec la base de données grâce à un client typé généré automatiquement. Voici l'intégration complète avec NestJS.
# terminal
# Installation de Prisma
npm install prisma @prisma/client
# Initialisation de Prisma avec PostgreSQL
npx prisma init --datasource-provider postgresqlDéfinition du schéma de données.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
firstName String @map("first_name")
lastName String @map("last_name")
phone String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
posts Post[]
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
authorId Int @map("author_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relation vers User
author User @relation(fields: [authorId], references: [id])
@@map("posts")
}Création d'un module Prisma réutilisable.
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
// Connexion automatique au démarrage du module
async onModuleInit() {
await this.$connect();
}
// Déconnexion propre à l'arrêt de l'application
async onModuleDestroy() {
await this.$disconnect();
}
}import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
// @Global rend le service disponible dans toute l'application
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}Mise à jour du service Users pour utiliser Prisma.
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from '@prisma/client';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
async create(createUserDto: CreateUserDto): Promise<Omit<User, 'password'>> {
// Vérification d'unicité de l'email
const existingUser = await this.prisma.user.findUnique({
where: { email: createUserDto.email },
});
if (existingUser) {
throw new ConflictException('Cet email est déjà utilisé');
}
// Hashage du mot de passe
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
// Création de l'utilisateur
const user = await this.prisma.user.create({
data: {
...createUserDto,
password: hashedPassword,
},
});
// Exclusion du mot de passe de la réponse
const { password, ...result } = user;
return result;
}
async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> {
// Exécution parallèle du count et de la requête paginée
const [data, total] = await Promise.all([
this.prisma.user.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
phone: true,
createdAt: true,
updatedAt: true,
},
}),
this.prisma.user.count(),
]);
return { data: data as User[], total };
}
async findOne(id: number): Promise<Omit<User, 'password'>> {
const user = await this.prisma.user.findUnique({
where: { id },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
phone: true,
createdAt: true,
updatedAt: true,
},
});
if (!user) {
throw new NotFoundException(`Utilisateur avec l'ID ${id} introuvable`);
}
return user as Omit<User, 'password'>;
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<Omit<User, 'password'>> {
// Vérification de l'existence
await this.findOne(id);
const user = await this.prisma.user.update({
where: { id },
data: updateUserDto,
select: {
id: true,
email: true,
firstName: true,
lastName: true,
phone: true,
createdAt: true,
updatedAt: true,
},
});
return user as Omit<User, 'password'>;
}
async remove(id: number): Promise<void> {
await this.findOne(id);
await this.prisma.user.delete({ where: { id } });
}
}Intercepteurs pour la transformation des réponses
Les intercepteurs permettent de transformer les réponses de manière uniforme. Un intercepteur de transformation standardise le format de toutes les réponses API.
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
// Interface pour le format de réponse standardisé
export interface ApiResponse<T> {
success: boolean;
data: T;
timestamp: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}Un intercepteur de logging trace les requêtes et leurs temps d'exécution.
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(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const now = Date.now();
return next.handle().pipe(
tap(() => {
const response = context.switchToHttp().getResponse();
const { statusCode } = response;
const duration = Date.now() - now;
// Log au format structuré
this.logger.log(
`${method} ${url} ${statusCode} - ${duration}ms`
);
}),
);
}
}Conclusion
NestJS offre une architecture robuste et scalable pour créer des APIs REST professionnelles. La combinaison de TypeScript, l'injection de dépendances et les décorateurs expressifs permet de construire des applications maintenables et testables.
Checklist pour une API NestJS de qualité
- ✅ Structure modulaire avec séparation des responsabilités
- ✅ DTOs avec validation class-validator pour toutes les entrées
- ✅ Services dédiés à la logique métier
- ✅ Gestion centralisée des erreurs avec filtres d'exceptions
- ✅ Intercepteurs pour la transformation et le logging
- ✅ Intégration Prisma pour les accès base de données
- ✅ ValidationPipe global avec whitelist activé
- ✅ Préfixe API cohérent sur toutes les routes
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
La force de NestJS réside dans sa structure opinionnée qui guide naturellement vers les bonnes pratiques. Les patterns éprouvés comme l'injection de dépendances et la séparation en couches produisent du code testable et évolutif, prêt pour les applications d'entreprise.
Tags
Partager
Articles similaires

NestJS + Prisma : La stack moderne pour le backend Node.js
Guide complet pour créer une API backend moderne avec NestJS et Prisma. Configuration, modèles, services, transactions et bonnes pratiques expliquées.

Questions d'entretien Node.js backend : Guide complet 2026
Les 25 questions d'entretien Node.js backend les plus fréquentes. Event loop, async/await, streams, clustering et performance expliqués avec des réponses détaillées.

NestJS en entretien : Guards, Interceptors et Architecture modulaire
Les questions d'entretien NestJS sur les Guards, Interceptors et l'architecture modulaire, avec des exemples de code concrets et des explications techniques.