Cours NestJS · Module 1: Bases de NestJS

Première API REST - opérations CRUD

7 min de lecture
Dans cette leçon9

Ave, bâtisseur de routes ! Le consul Caesar.js te confie l'une des tâches les plus importantes de l'Empire - construire le réseau routier de l'API REST. Tout comme les routes romaines reliaient toutes les provinces de l'Empire, l'API REST relie notre application serveur au monde extérieur. Il est temps d'apprendre les opérations CRUD - le fondement de toute API REST !

Qu'est-ce que le CRUD ?

CRUD est un acronyme désignant quatre opérations de données de base :

  • Create (créer) - recruter de nouveaux légionnaires
  • Read (lire) - parcourir les registres de l'Empire
  • Update (mettre à jour) - promouvoir des légionnaires
  • Delete (supprimer) - retirer des registres

Chaque opération CRUD correspond à une méthode HTTP et à un décorateur NestJS spécifiques :

OpérationMéthode HTTPDécorateur NestJS
CreatePOST@Post()
ReadGET@Get()
UpdatePUT / PATCH@Put() / @Patch()
DeleteDELETE@Delete()

Construire un contrôleur CRUD étape par étape

Construisons un contrôleur complet pour gérer les légionnaires de notre Empire :

1import {
2  Controller, Get, Post, Put, Delete, Patch,
3  Param, Body, Query, HttpCode, HttpStatus, NotFoundException
4} from '@nestjs/common';
5
6// Interface Legionary
7interface Legionary {
8  id: number;
9  name: string;
10  rank: string;
11  province: string;
12}
13
14@Controller('legionaries')
15export class LegionariesController {
16  // Tableau temporaire faisant office de "base de données"
17  private legionaries: Legionary[] = [
18    { id: 1, name: 'Marcus Aurelius', rank: 'Centurio', province: 'Roma' },
19    { id: 2, name: 'Gaius Julius', rank: 'Miles', province: 'Gallia' },
20  ];
21  // Prochain numéro libre (1 et 2 sont pris)
22  private nextId = 3;
23}

READ - lire les données (@Get)

Le décorateur @Get() gère les requêtes HTTP GET. Nous l'utilisons pour récupérer des données :

1@Controller('legionaries')
2export class LegionariesController {
3
4  // GET /legionaries - récupère tous les légionnaires
5  @Get()
6  findAll(): Legionary[] {
7    return this.legionaries;
8  }
9
10  // GET /legionaries/search?province=Roma - filtre par province
11  @Get('search')
12  findByProvince(@Query('province') province: string): Legionary[] {
13    return this.legionaries.filter(l => l.province === province);
14  }
15
16  // GET /legionaries/1 - récupère un légionnaire par ID
17  @Get(':id')
18  findOne(@Param('id') id: string): Legionary {
19    const legionary = this.legionaries.find(l => l.id === Number(id));
20    if (!legionary) {
21      throw new NotFoundException('Légionnaire introuvable !');
22    }
23    return legionary;
24  }
25}

Remarque ces trois décorateurs de paramètres essentiels :

  • @Param('id') - extrait un paramètre de l'URL (par exemple /legionaries/1)
  • @Query('province') - extrait un paramètre de requête (par exemple ?province=Roma)
  • @Body() - extrait les données du corps de la requête (pour POST/PUT)

Deux choses dans ce code sont voulues. La route search se trouve avant :id - sinon GET /legionaries/search arriverait dans findOne avec id égal à 'search'. Et un légionnaire absent est signalé par l'exception NotFoundException de @nestjs/common : NestJS répond alors avec le code 404 Not Found. Un simple throw new Error(...) donnerait au client 500 Internal Server Error, comme si le serveur était en panne.

CREATE - créer des données (@Post)

Le décorateur @Post() gère les requêtes HTTP POST. Il sert à créer de nouvelles ressources :

1// POST /legionaries - recrute un nouveau légionnaire
2@Post()
3@HttpCode(HttpStatus.CREATED) // Renvoie le statut 201
4create(@Body() newLegionary: { name: string; rank: string; province: string }): Legionary {
5  const legionary: Legionary = {
6    id: this.nextId++,
7    ...newLegionary,
8  };
9  this.legionaries.push(legionary);
10  return legionary;
11}

Le décorateur @Body() extrait les données du corps JSON de la requête. Le décorateur @HttpCode() te permet de définir le code de réponse HTTP - pour la création d'une ressource, le standard est 201 Created.

Le numéro du nouveau légionnaire vient du compteur nextId de la classe du contrôleur. Il augmente à chaque recrutement, donc il ne se répète jamais, même après la radiation de quelqu'un du tableau - un numéro calculé comme length + 1 pourrait déjà appartenir à un autre légionnaire.

UPDATE - mettre à jour les données (@Put et @Patch)

Nous avons deux décorateurs pour la mise à jour :

  • @Put() - remplace l'ensemble de la ressource (mise à jour complète)
  • @Patch() - met à jour uniquement certains champs (mise à jour partielle)
1// PUT /legionaries/1 - mise à jour complète d'un légionnaire
2@Put(':id')
3update(
4  @Param('id') id: string,
5  @Body() updateData: { name: string; rank: string; province: string },
6): Legionary {
7  const index = this.legionaries.findIndex(l => l.id === Number(id));
8  if (index === -1) {
9    throw new NotFoundException('Légionnaire introuvable !');
10  }
11  this.legionaries[index] = { id: Number(id), ...updateData };
12  return this.legionaries[index];
13}
14
15// PATCH /legionaries/1 - mise à jour partielle (par exemple, le rang seulement)
16@Patch(':id')
17partialUpdate(
18  @Param('id') id: string,
19  @Body() updateData: Partial<Legionary>,
20): Legionary {
21  const index = this.legionaries.findIndex(l => l.id === Number(id));
22  if (index === -1) {
23    throw new NotFoundException('Légionnaire introuvable !');
24  }
25  this.legionaries[index] = { ...this.legionaries[index], ...updateData };
26  return this.legionaries[index];
27}

DELETE - supprimer les données (@Delete)

Le décorateur @Delete() gère les requêtes HTTP DELETE :

1// DELETE /legionaries/1 - supprime un légionnaire
2@Delete(':id')
3@HttpCode(HttpStatus.NO_CONTENT) // Renvoie le statut 204
4remove(@Param('id') id: string): void {
5  const index = this.legionaries.findIndex(l => l.id === Number(id));
6  if (index === -1) {
7    throw new NotFoundException('Légionnaire introuvable !');
8  }
9  this.legionaries.splice(index, 1);
10}

Pour les opérations de suppression, le code de réponse standard est 204 No Content - il signifie un succès sans renvoi de données.

Codes de statut HTTP

Connaître les codes HTTP est un devoir pour tout bâtisseur d'API :

CodeNomQuand l'utiliser
200OKSuccès (par défaut pour GET, PUT, PATCH)
201CreatedUne nouvelle ressource a été créée (POST)
204No ContentSuccès sans corps de réponse (DELETE)
400Bad RequestDonnées d'entrée invalides
404Not FoundLa ressource n'existe pas
500Internal Server ErrorErreur serveur

Contrôleur + Service - le bon patron

Dans un véritable Empire, le contrôleur n'exécute pas lui-même la logique - il la délègue à un service :

1// legionaries.service.ts
2@Injectable()
3export class LegionariesService {
4  private legionaries: Legionary[] = [];
5  private nextId = 1;
6
7  findAll(): Legionary[] {
8    return this.legionaries;
9  }
10
11  findOne(id: number): Legionary {
12    return this.legionaries.find(l => l.id === id);
13  }
14
15  create(data: { name: string; rank: string; province: string }): Legionary {
16    const legionary = { id: this.nextId++, ...data };
17    this.legionaries.push(legionary);
18    return legionary;
19  }
20
21  update(id: number, data: Partial<Legionary>): Legionary {
22    const index = this.legionaries.findIndex(l => l.id === id);
23    this.legionaries[index] = { ...this.legionaries[index], ...data };
24    return this.legionaries[index];
25  }
26
27  remove(id: number): void {
28    this.legionaries = this.legionaries.filter(l => l.id !== id);
29  }
30}
31
32// legionaries.controller.ts
33@Controller('legionaries')
34export class LegionariesController {
35  constructor(private readonly legionariesService: LegionariesService) {}
36
37  @Get()
38  findAll() {
39    return this.legionariesService.findAll();
40  }
41
42  @Post()
43  create(@Body() data: { name: string; rank: string; province: string }) {
44    return this.legionariesService.create(data);
45  }
46}

Tester avec curl

Tu peux tester ton API à l'aide de l'outil curl dans le terminal :

1# GET - récupère tout
2curl http://localhost:3000/legionaries
3
4# POST - crée un nouveau
5curl -X POST http://localhost:3000/legionaries \
6  -H "Content-Type: application/json" \
7  -d '{"name": "Titus Flavius", "rank": "Miles", "province": "Judea"}'
8
9# PUT - mise à jour complète
10curl -X PUT http://localhost:3000/legionaries/1 \
11  -H "Content-Type: application/json" \
12  -d '{"name": "Marcus Aurelius", "rank": "Legatus", "province": "Roma"}'
13
14# DELETE - supprime
15curl -X DELETE http://localhost:3000/legionaries/1

Bien joué, bâtisseur ! Tu connais maintenant les fondamentaux de l'API REST - les routes qui relient toutes les provinces de notre Empire NestJS !

Code de cette leçon : src/crud-controller.ts
1// Première API REST - Opérations CRUD dans l'Imperium
2import {
3  Controller, Get, Post, Put, Delete, Patch,
4  Param, Body, Query, HttpCode, HttpStatus, Injectable
5} from '@nestjs/common';
6
7console.log("API REST CRUD - les routes de l'imperium !");
8
9// ===========================================
10// 1. Interface et données
11// ===========================================
12
13interface Legionary {
14  id: number;
15  name: string;
16  rank: string;
17  province: string;
18}
19
20const legionaries: Legionary[] = [
21  { id: 1, name: 'Marcus Aurelius', rank: 'Centurio', province: 'Roma' },
22  { id: 2, name: 'Gaius Julius', rank: 'Miles', province: 'Gallia' },
23  { id: 3, name: 'Titus Flavius', rank: 'Optio', province: 'Judea' },
24];
25
26// ===========================================
27// 2. READ - @Get() - récupération des données
28// ===========================================
29
30// GET /legionaries - liste tout
31// @Get()
32function findAll(): Legionary[] {
33  return legionaries;
34}
35
36// GET /legionaries/:id - un par ID
37// @Get(':id')
38function findOne(id: string): Legionary | undefined {
39  return legionaries.find(l => l.id === Number(id));
40}
41
42// GET /legionaries?province=Roma - filtrage
43// @Get('search')
44function findByProvince(province: string): Legionary[] {
45  return legionaries.filter(l => l.province === province);
46}
47
48console.log("GET /legionaries :", findAll());
49console.log("GET /legionaries/1 :", findOne('1'));
50console.log("GET ?province=Roma :", findByProvince('Roma'));
51
52// ===========================================
53// 3. CREATE - @Post() - création de données
54// ===========================================
55
56// POST /legionaries
57function create(data: { name: string; rank: string; province: string }): Legionary {
58  const newLegionary: Legionary = {
59    id: legionaries.length + 1,
60    ...data,
61  };
62  legionaries.push(newLegionary);
63  return newLegionary;
64}
65
66const created = create({ name: 'Lucius Verus', rank: 'Miles', province: 'Syria' });
67console.log("\nPOST - nouveau légionnaire :", created);
68
69// ===========================================
70// 4. UPDATE - @Put() / @Patch()
71// ===========================================
72
73// PUT /legionaries/:id - mise à jour complète
74function update(id: string, data: Omit<Legionary, 'id'>): Legionary | null {
75  const index = legionaries.findIndex(l => l.id === Number(id));
76  if (index === -1) return null;
77  legionaries[index] = { id: Number(id), ...data };
78  return legionaries[index];
79}
80
81// PATCH /legionaries/:id - mise à jour partielle
82function partialUpdate(id: string, data: Partial<Legionary>): Legionary | null {
83  const index = legionaries.findIndex(l => l.id === Number(id));
84  if (index === -1) return null;
85  legionaries[index] = { ...legionaries[index], ...data };
86  return legionaries[index];
87}
88
89console.log("\nPUT /legionaries/2 :", update('2', { name: 'Gaius Julius', rank: 'Centurio', province: 'Roma' }));
90console.log("PATCH /legionaries/3 :", partialUpdate('3', { rank: 'Centurio' }));
91
92// ===========================================
93// 5. DELETE - @Delete() - suppression de données
94// ===========================================
95
96function remove(id: string): boolean {
97  const index = legionaries.findIndex(l => l.id === Number(id));
98  if (index === -1) return false;
99  legionaries.splice(index, 1);
100  return true;
101}
102
103console.log("\nDELETE /legionaries/4 :", remove('4'));
104console.log("État après les opérations :", legionaries);
105
106// ===========================================
107// 6. Codes de statut HTTP
108// ===========================================
109
110console.log("\n=== CODES DE STATUT HTTP ===");
111console.log("200 OK - succès (GET, PUT, PATCH)");
112console.log("201 Created - ressource créée (POST)");
113console.log("204 No Content - succès sans corps (DELETE)");
114console.log("400 Bad Request - données invalides");
115console.log("404 Not Found - la ressource n'existe pas");
116
117console.log("\n=== DÉCORATEURS CRUD ===");
118console.log("@Get()    -> lire les données");
119console.log("@Post()   -> créer les données");
120console.log("@Put()    -> mise à jour complète");
121console.log("@Patch()  -> mise à jour partielle");
122console.log("@Delete() -> supprimer les données");
123console.log("@Param()  -> paramètre de l'URL");
124console.log("@Body()   -> données du corps de la requête");
125console.log("@Query()  -> paramètre de requête");
126

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que signifie l'acronyme CRUD ?

  2. 2. Quel code de réponse NestJS renvoie-t-il par défaut depuis une méthode marquée du décorateur @Post() ?

  3. 3. À quoi sert le décorateur @Param() dans un contrôleur NestJS ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Dans un contrôleur LegionController avec le décorateur @Controller('legionaries'), garde un tableau de légionnaires, par exemple [{ id: 1, name: 'Marcus Aurelius' }]. Écris une méthode findOne avec @Get(':id') qui lit l'id via @Param('id') et renvoie le légionnaire avec cet id, et lève NotFoundException s'il n'existe pas. N'oublie pas que l'id de l'URL est une chaîne.

  • Tri horizontal

    Remets dans l'ordre les éléments de la méthode POST dans un contrôleur NestJS :

  • Tri vertical

    Range les codes de statut HTTP du succès à l'erreur serveur :

  • Éditeur de code

    Dans un contrôleur LegionController avec le décorateur @Controller('legionaries') et un tableau de légionnaires, écris une méthode remove avec @Delete(':id') et @HttpCode(HttpStatus.NO_CONTENT) qui lit l'id via @Param('id') et supprime du tableau le légionnaire avec cet id. La méthode ne renvoie rien.