Un CRUD (Create, Read, Update, Delete) est la base de la majorité des API REST. NestJS fournit une structure très guidée (modules, controllers, services, DTO) qui permet de construire rapidement des endpoints robustes, validés et testables.
Dans ce tutoriel, on va créer (ou recréer) un CRUD REST complet pour une ressource Services en s’appuyant sur la génération via le CLI (nest generate resource), puis en implémentant chaque opération avec les bons codes HTTP, la validation des données et des exemples de tests via curl (ou Postman).
Prérequis : Node.js récent, NestJS CLI installé, et une base NestJS opérationnelle. Côté validation, on part du principe que le projet utilise class-validator / class-transformer (classique en NestJS). Pour les liens de référence, voir la documentation officielle NestJS : https://docs.nestjs.com/.
1) Générer la ressource avec le CLI
NestJS propose un générateur de ressource qui crée automatiquement la structure standard (module, controller, service, DTO, entity) et des méthodes “placeholder” pour le CRUD.
Depuis la racine du projet :
nest generate resource services
Le CLI va vous poser quelques questions (type de transport REST/GraphQL, CRUD, etc.). Pour un CRUD REST classique, choisissez :
- Transport layer : REST API
- Generate CRUD entry points : Yes
À ce stade, vous obtenez un squelette fonctionnel, mais généralement sans persistance (ou avec des méthodes vides). L’idée est de compléter en respectant les conventions NestJS et le style du module ServicesModule du projet un projet de production.
2) Comprendre la structure générée
Le générateur crée typiquement une arborescence proche de :
src/
services/
dto/
create-service.dto.ts
update-service.dto.ts
entities/
service.entity.ts
services.controller.ts
services.module.ts
services.service.ts
2.1 Le module
Le module regroupe tout ce qui concerne la ressource : controller(s), provider(s), imports/exports. C’est l’équivalent d’un “dossier fonctionnel” qui encapsule une feature.
// src/services/services.module.ts
import { Module } from '@nestjs/common';
import { ServicesService } from './services.service';
import { ServicesController } from './services.controller';
@Module({
controllers: [ServicesController],
providers: [ServicesService],
})
export class ServicesModule {}
2.2 Le controller
Le controller expose les routes HTTP et délègue la logique métier au service. Il doit rester fin : validation d’entrée (via DTO/pipes), mapping HTTP (codes, paramètres), et rien de plus.
2.3 Le service
Le service contient la logique métier et l’accès aux données (via un repository, Prisma/TypeORM, ou une couche dédiée). C’est le cœur de votre CRUD.
2.4 DTO : Create / Update
Les DTO (Data Transfer Objects) définissent la forme des données attendues en entrée. Avec class-validator, ils permettent d’appliquer des règles (ex. champ requis, longueur, format).
2.5 Entity
L’entity représente la structure “métier” d’un service. Dans un projet avec ORM, elle mappe souvent une table (TypeORM) ou un modèle (Prisma). Même sans ORM, elle sert de contrat interne.
3) Mettre en place la validation des requêtes (indispensable)
Avant d’implémenter les endpoints, assurez-vous que la validation est activée globalement (souvent dans main.ts) via le ValidationPipe. Cela garantit que les DTO sont appliqués automatiquement.
Pourquoi c’est important ? Sans validation, votre API accepte n’importe quoi (types incorrects, champs manquants). Avec la validation, vous “verrouillez” le contrat HTTP, comme une interface stricte.
// Exemple typique (main.ts)
import { ValidationPipe } from '@nestjs/common';
// ... dans bootstrap()
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // supprime les propriétés inconnues
forbidNonWhitelisted: true, // rejette si propriétés inconnues
transform: true, // convertit les types (params, query) si possible
}),
);
4) Implémenter CREATE (POST /services)
Le Create correspond à un @Post() qui reçoit un corps JSON validé par un CreateServiceDto. En REST, la réponse standard est :
- 201 Created si la création réussit
- Un body contenant l’objet créé (souvent), ou au minimum son identifiant
4.1 DTO de création
Adaptez les champs à votre modèle “Service”. Exemple typique :
// src/services/dto/create-service.dto.ts
import { IsBoolean, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
export class CreateServiceDto {
@IsString()
@IsNotEmpty()
@MaxLength(120)
name: string;
@IsString()
@IsOptional()
@MaxLength(500)
description?: string;
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
4.2 Controller : route POST
// src/services/services.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { ServicesService } from './services.service';
import { CreateServiceDto } from './dto/create-service.dto';
@Controller('services')
export class ServicesController {
constructor(private readonly servicesService: ServicesService) {}
@Post()
create(@Body() dto: CreateServiceDto) {
// NestJS renverra 201 par défaut sur un POST si vous ne changez pas le code
return this.servicesService.create(dto);
}
}
4.3 Service : logique de création
Dans un projet réel, vous persistez en base. Ici, le point important est : le service retourne l’objet créé (ou une représentation) et gère les valeurs par défaut.
// src/services/services.service.ts
import { Injectable } from '@nestjs/common';
import { CreateServiceDto } from './dto/create-service.dto';
@Injectable()
export class ServicesService {
async create(dto: CreateServiceDto) {
// Exemple : normalisation / valeurs par défaut
const service = {
id: crypto.randomUUID(),
name: dto.name,
description: dto.description ?? null,
isActive: dto.isActive ?? true,
createdAt: new Date().toISOString(),
};
// TODO: persister en base (repository/ORM)
return service;
}
}
5) Implémenter READ (GET /services et GET /services/:id)
Le Read se décline en deux endpoints :
GET /services: liste paginée ou complèteGET /services/:id: détail d’un élément
Côté codes HTTP :
- 200 OK si tout va bien
- 404 Not Found si l’élément demandé n’existe pas
5.1 Controller : liste et détail
import { Controller, Get, Param } from '@nestjs/common';
import { ServicesService } from './services.service';
@Controller('services')
export class ServicesController {
constructor(private readonly servicesService: ServicesService) {}
@Get()
findAll() {
return this.servicesService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.servicesService.findOne(id);
}
}
5.2 Service : 404 si absent
Le standard NestJS consiste à lever une NotFoundException. Le framework convertit automatiquement en réponse HTTP 404.
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class ServicesService {
// TODO: remplacer par un accès DB
private readonly items: any[] = [];
async findAll() {
return this.items;
}
async findOne(id: string) {
const item = this.items.find((x) => x.id === id);
if (!item) {
throw new NotFoundException(`Service ${id} introuvable`);
}
return item;
}
}
6) Implémenter UPDATE (PATCH vs PUT)
La mise à jour est souvent la partie la plus mal comprise :
- PUT : remplacement complet de la ressource (vous envoyez “tout” l’objet)
- PATCH : mise à jour partielle (vous envoyez seulement les champs à modifier)
En pratique, beaucoup d’API REST modernes utilisent PATCH pour la souplesse. NestJS génère souvent un @Patch(':id') par défaut.
6.1 DTO de mise à jour
Le pattern standard NestJS : UpdateXxxDto hérite d’un DTO partiel (partial type) du DTO de création. Cela rend tous les champs optionnels.
// src/services/dto/update-service.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateServiceDto } from './create-service.dto';
export class UpdateServiceDto extends PartialType(CreateServiceDto) {}
6.2 Controller : PATCH
import { Body, Controller, Param, Patch } from '@nestjs/common';
import { ServicesService } from './services.service';
import { UpdateServiceDto } from './dto/update-service.dto';
@Controller('services')
export class ServicesController {
constructor(private readonly servicesService: ServicesService) {}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateServiceDto) {
return this.servicesService.update(id, dto);
}
}
6.3 Service : mise à jour partielle + 404
import { Injectable, NotFoundException } from '@nestjs/common';
import { UpdateServiceDto } from './dto/update-service.dto';
@Injectable()
export class ServicesService {
private readonly items: any[] = [];
async update(id: string, dto: UpdateServiceDto) {
const index = this.items.findIndex((x) => x.id === id);
if (index === -1) {
throw new NotFoundException(`Service ${id} introuvable`);
}
// PATCH: on merge les champs fournis
const updated = {
...this.items[index],
...dto,
updatedAt: new Date().toISOString(),
};
this.items[index] = updated;
return updated; // 200 OK
}
}
7) Implémenter DELETE (hard delete vs soft delete)
La suppression peut être :
- Hard delete : suppression définitive (ligne supprimée)
- Soft delete : suppression logique (ex.
deletedAtrempli, ouisActive=false)
Le soft delete est souvent préférable en production (audit, restauration, cohérence des relations). Le hard delete est utile pour des données purement techniques ou temporaires.
7.1 Controller : DELETE /services/:id
Deux options courantes :
- Retourner l’objet supprimé (200 OK)
- Ne rien retourner et utiliser 204 No Content (souvent plus “REST”)
Pour 204, on peut fixer explicitement le code :
import { Controller, Delete, HttpCode, Param } from '@nestjs/common';
import { ServicesService } from './services.service';
@Controller('services')
export class ServicesController {
constructor(private readonly servicesService: ServicesService) {}
@Delete(':id')
@HttpCode(204)
async remove(@Param('id') id: string) {
await this.servicesService.remove(id);
}
}
7.2 Service : suppression + 404
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class ServicesService {
private readonly items: any[] = [];
async remove(id: string) {
const index = this.items.findIndex((x) => x.id === id);
if (index === -1) {
throw new NotFoundException(`Service ${id} introuvable`);
}
// Hard delete
this.items.splice(index, 1);
}
}
7.3 Variante : soft delete
En soft delete, on ne retire pas l’élément : on le marque comme supprimé, puis on l’exclut des findAll (et parfois de findOne selon votre besoin).
async softRemove(id: string) {
const item = await this.findOne(id);
item.deletedAt = new Date().toISOString();
return item;
}
async findAll() {
return this.items.filter((x) => !x.deletedAt);
}
8) Récapitulatif des codes HTTP à respecter
- POST /services → 201 Created (+ body de l’objet créé)
- GET /services → 200 OK (+ liste)
- GET /services/:id → 200 OK (+ objet) ou 404 Not Found
- PATCH /services/:id → 200 OK (+ objet mis à jour) ou 404 Not Found
- DELETE /services/:id → 204 No Content ou 404 Not Found
9) Tester le CRUD avec curl (ou Postman)
Une fois le serveur démarré (souvent npm run start:dev), vous pouvez tester chaque endpoint.
9.1 CREATE
curl -i -X POST http://localhost:3000/services \
-H "Content-Type: application/json" \
-d '{
"name": "Consultation production",
"description": "Session de 30 minutes",
"isActive": true
}'
Vérifiez le HTTP/1.1 201 et le JSON retourné.
9.2 READ (liste)
curl -i http://localhost:3000/services
9.3 READ (détail)
curl -i http://localhost:3000/services/<SERVICE_ID>
9.4 UPDATE (PATCH)
curl -i -X PATCH http://localhost:3000/services/<SERVICE_ID> \
-H "Content-Type: application/json" \
-d '{
"description": "Session de 45 minutes"
}'
9.5 DELETE
curl -i -X DELETE http://localhost:3000/services/<SERVICE_ID>
Vous devez obtenir un 204 No Content (sans body).
10) Pièges courants et bonnes pratiques
- Oublier la validation : sans
ValidationPipe, vos DTO ne protègent rien. - Mélanger controller et service : gardez le controller “HTTP” et le service “métier”.
- PATCH mal géré : n’écrasez pas des champs à
nullpar accident. Faites une fusion contrôlée. - Delete définitif trop tôt : privilégiez le soft delete si vous avez des contraintes légales/audit.
- 404 vs 200 vide : pour
GET /:id, une ressource absente doit être 404, pasnullen 200.
Conclusion
Vous avez maintenant un CRUD REST complet dans NestJS pour la ressource Services, structuré selon les conventions du framework : module pour l’encapsulation, controller pour l’interface HTTP, service pour la logique métier, et DTO pour la validation.
Pour aller plus loin, vous pouvez :
- Brancher une vraie persistance (Prisma/TypeORM) et remplacer le stockage en mémoire
- Ajouter la pagination, le tri et le filtrage sur
GET /services - Écrire des tests (unitaires pour le service, e2e pour le controller)
- Sécuriser les routes (guards JWT, rôles)
Référence : documentation officielle NestJS https://docs.nestjs.com/.

Commentaires