Cours NestJS · Module 1: Bases de NestJS
PROJET : le Registre des légions de l'Empire
Dans cette leçon8
Ave, légat ! Tout au long du module, tu as construit des éléments isolés : un contrôleur ici, un service là, un DTO et la configuration à part. Une vraie application n'est pas un tas de briques éparses - c'est un seul organisme dans lequel une requête passe par plusieurs mains avant que la réponse ne revienne. Il est temps de tout assembler : le Registre des légions, une API dans laquelle la chancellerie de l'Empire reçoit, vérifie et conserve les données des légionnaires.
Ce que tu vas construire
Une API REST avec cinq points de terminaison sous l'adresse /legionaries :
| Méthode | Adresse | Ce qu'elle fait | Code de réponse |
|---|---|---|---|
| GET | /legionaries | liste de tous les légionnaires | 200 |
| GET | /legionaries/search?province=Gallia | légionnaires d'une province | 200 |
| GET | /legionaries/:id | un légionnaire | 200 ou 404 |
| POST | /legionaries | recrutement d'un nouveau légionnaire | 201 ou 400 |
| DELETE | /legionaries/:id | radiation du registre | 204 ou 404 |
Tu as rencontré chaque élément de ce tableau dans ce module. Le projet vérifie que tu sais les réunir dans une seule application.
Étape 1 : le projet et les paquets
Commence par un nouveau projet (le générateur nest new de NestJS 12 a besoin de Node.js 22.22.3+ ou 24.15+) et installe les paquets que nous avons utilisés dans les leçons sur les DTO et la configuration :
1npm install -g @nestjs/cli
2nest new legion-register
3cd legion-register
4npm i @nestjs/config joi class-validator class-transformer @nestjs/mapped-typesCrée ensuite un fichier .env dans le dossier du projet. N'oublie pas qu'il ne va pas dans le dépôt - ajoute-le à .gitignore, et enregistre à côté un .env.example avec les mêmes clés :
1PORT=3000
2LEGION_NAME=Legio X EquestrisÉtape 2 : le DTO - le formulaire de recrutement
Le registre n'accepte que des candidatures complètes. Décris-les avec une classe DTO et des décorateurs de class-validator :
1// legion/dto/create-legionary.dto.ts
2import { IsEmail, IsIn, IsNotEmpty, IsString } from 'class-validator';
3
4export class CreateLegionaryDto {
5 @IsString()
6 @IsNotEmpty()
7 name: string;
8
9 @IsIn(['Miles', 'Optio', 'Centurio'])
10 rank: string;
11
12 @IsString()
13 @IsNotEmpty()
14 province: string;
15
16 @IsEmail()
17 email: string;
18}Les décorateurs seuls ne bloquent encore rien. Ils n'agissent qu'avec ValidationPipe, que tu activeras à l'étape 5.
Étape 3 : le service - la chancellerie du registre
Toute la logique vit dans le service. Il garde les légionnaires dans un tableau, leur attribue des numéros successifs et signale un légionnaire absent avec l'exception NotFoundException. Il lit le nom de la légion dans la configuration grâce à ConfigService :
1// legion/legion.service.ts
2import { Injectable, NotFoundException } from '@nestjs/common';
3import { ConfigService } from '@nestjs/config';
4import { CreateLegionaryDto } from './dto/create-legionary.dto';
5
6export interface Legionary extends CreateLegionaryDto {
7 id: number;
8}
9
10@Injectable()
11export class LegionService {
12 private legionaries: Legionary[] = [];
13 private nextId = 1;
14
15 constructor(private readonly configService: ConfigService) {}
16
17 findAll(): Legionary[] {
18 return this.legionaries;
19 }
20
21 findByProvince(province: string): Legionary[] {
22 return this.legionaries.filter((l) => l.province === province);
23 }
24
25 findOne(id: number): Legionary {
26 const legionary = this.legionaries.find((l) => l.id === id);
27 if (!legionary) {
28 throw new NotFoundException('Aucun légionnaire avec le numéro ' + id);
29 }
30 return legionary;
31 }
32
33 recruit(data: CreateLegionaryDto): Legionary {
34 const legionary = { id: this.nextId++, ...data };
35 this.legionaries.push(legionary);
36 return legionary;
37 }
38
39 dismiss(id: number): void {
40 this.findOne(id);
41 this.legionaries = this.legionaries.filter((l) => l.id !== id);
42 }
43
44 getLegionName(): string {
45 return this.configService.get('LEGION_NAME');
46 }
47}Remarque le compteur nextId. Un numéro tiré de la longueur du tableau se répéterait après la radiation de quelqu'un au milieu du registre, alors que le compteur ne fait qu'augmenter. La méthode dismiss appelle d'abord findOne, donc supprimer un légionnaire qui n'existe pas se termine aussi par une réponse 404.
Étape 4 : le contrôleur - un centurion à la porte
Le contrôleur ne fait que recevoir les requêtes et les transmettre au service. Surveille l'ordre des routes : search avant :id, sinon l'adresse /legionaries/search arriverait dans findOne :
1// legion/legion.controller.ts
2import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query } from '@nestjs/common';
3import { CreateLegionaryDto } from './dto/create-legionary.dto';
4import { LegionService } from './legion.service';
5
6@Controller('legionaries')
7export class LegionController {
8 constructor(private readonly legionService: LegionService) {}
9
10 @Get()
11 findAll() {
12 return this.legionService.findAll();
13 }
14
15 @Get('search')
16 search(@Query('province') province: string) {
17 return this.legionService.findByProvince(province);
18 }
19
20 @Get(':id')
21 findOne(@Param('id') id: string) {
22 return this.legionService.findOne(Number(id));
23 }
24
25 @Post()
26 recruit(@Body() dto: CreateLegionaryDto) {
27 return this.legionService.recruit(dto);
28 }
29
30 @Delete(':id')
31 @HttpCode(HttpStatus.NO_CONTENT)
32 dismiss(@Param('id') id: string) {
33 this.legionService.dismiss(Number(id));
34 }
35}L'id de l'adresse est du texte, le contrôleur le convertit donc en nombre avec Number(id) avant de le passer au service. Inutile de définir le code 201 pour @Post() - NestJS le choisit lui-même.
Étape 5 : les modules et le démarrage de l'application
Le module de la légion enregistre le contrôleur et le service :
1// legion/legion.module.ts
2import { Module } from '@nestjs/common';
3import { LegionController } from './legion.controller';
4import { LegionService } from './legion.service';
5
6@Module({
7 controllers: [LegionController],
8 providers: [LegionService],
9})
10export class LegionModule {}Le module racine charge la configuration, la vérifie au démarrage avec un schéma Joi et ajoute le module de la légion :
1// app.module.ts
2import { Module } from '@nestjs/common';
3import { ConfigModule } from '@nestjs/config';
4import * as Joi from 'joi';
5import { LegionModule } from './legion/legion.module';
6
7@Module({
8 imports: [
9 ConfigModule.forRoot({
10 isGlobal: true,
11 validationSchema: Joi.object({
12 PORT: Joi.number().default(3000),
13 LEGION_NAME: Joi.string().required(),
14 }),
15 }),
16 LegionModule,
17 ],
18})
19export class AppModule {}Enfin, main.ts active la validation pour toute l'application et démarre le serveur :
1// main.ts
2import { ValidationPipe } from '@nestjs/common';
3import { NestFactory } from '@nestjs/core';
4import { AppModule } from './app.module';
5
6async function bootstrap() {
7 const app = await NestFactory.create(AppModule);
8 app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
9 await app.listen(process.env.PORT ?? 3000);
10}
11bootstrap();Vérifie le registre
Lance le serveur avec npm run start:dev et teste les points de terminaison dans un second terminal :
1curl -X POST http://localhost:3000/legionaries \
2 -H "Content-Type: application/json" \
3 -d '{"name": "Titus Pullo", "rank": "Miles", "province": "Gallia", "email": "titus@legio.rome"}'
4
5curl "http://localhost:3000/legionaries/search?province=Gallia"
6curl http://localhost:3000/legionaries/1
7curl -X DELETE http://localhost:3000/legionaries/1
8curl http://localhost:3000/legionaries/1La dernière commande doit renvoyer 404, car le légionnaire a été radié. Envoie aussi une candidature avec une adresse e-mail incorrecte ou un grade hors de la liste - ValidationPipe répondra avec le code 400 et une liste d'erreurs.
Ce que tu rends
Pousse le projet sur GitHub et envoie le lien dans la tâche suivante. Dans le README, décris les points de terminaison et la façon de lancer le projet : npm install, un fichier .env créé à partir de .env.example et npm run start:dev. Pour les plus motivés : ajoute PATCH /legionaries/:id avec un UpdateLegionaryDto basé sur PartialType, comme dans la leçon sur les DTO.
Souviens-toi, légat : dans un Empire bien bâti, chacun connaît sa place - le contrôleur reçoit, le service exécute, le DTO garde la porte et la configuration vit en dehors du code.
Code de cette leçon : src/imperium-project.ts
1// PROJET : Application complète Imperium Romanum
2import { Module, Controller, Injectable, Get, Post, Body, Param } from '@nestjs/common';
3
4console.log("=== PROJET : Imperium Management System ===");
5
6// ===========================================
7// 1. Module de légion
8// ===========================================
9
10interface Legion {
11 id: number;
12 name: string;
13 commander: string;
14 soldiers: number;
15 province: string;
16}
17
18@Injectable()
19export class LegionService {
20 private legions: Legion[] = [
21 { id: 1, name: 'Legio X Gemina', commander: 'Caesar', soldiers: 5000, province: 'Gallia' },
22 { id: 2, name: 'Legio III Augusta', commander: 'Scipio', soldiers: 4500, province: 'Africa' },
23 ];
24
25 findAll(): Legion[] { return this.legions; }
26
27 findById(id: number): Legion | undefined {
28 return this.legions.find(l => l.id === id);
29 }
30
31 create(data: Omit<Legion, 'id'>): Legion {
32 const legion = { id: this.legions.length + 1, ...data };
33 this.legions.push(legion);
34 return legion;
35 }
36}
37
38@Controller('legiones')
39export class LegionController {
40 constructor(private legionService: LegionService) {}
41
42 @Get()
43 findAll() { return this.legionService.findAll(); }
44
45 @Get(':id')
46 findOne(@Param('id') id: string) { return this.legionService.findById(+id); }
47}
48
49@Module({
50 controllers: [LegionController],
51 providers: [LegionService],
52 exports: [LegionService],
53})
54export class LegionModule {}
55
56// ===========================================
57// 2. Module de tributs
58// ===========================================
59
60interface Tribute {
61 id: number;
62 province: string;
63 amount: number;
64 type: 'gold' | 'silver' | 'goods';
65 collectedAt: string;
66}
67
68@Injectable()
69export class TributeService {
70 private tributes: Tribute[] = [
71 { id: 1, province: 'Gallia', amount: 50000, type: 'gold', collectedAt: '44 BC' },
72 { id: 2, province: 'Aegyptus', amount: 80000, type: 'gold', collectedAt: '30 BC' },
73 ];
74
75 findAll() { return this.tributes; }
76 getTotal() { return this.tributes.reduce((sum, t) => sum + t.amount, 0); }
77}
78
79// ===========================================
80// 3. Module de province
81// ===========================================
82
83@Injectable()
84export class ProvinciaService {
85 private provinces = [
86 { name: 'Gallia', governor: 'Gaius Julius Caesar', population: 120000 },
87 { name: 'Aegyptus', governor: 'Gaius Cornelius Gallus', population: 300000 },
88 { name: 'Britannia', governor: 'Aulus Plautius', population: 80000 },
89 ];
90
91 findAll() { return this.provinces; }
92}
93
94// ===========================================
95// Module principal de l'application
96// ===========================================
97
98@Module({
99 imports: [LegionModule],
100 providers: [TributeService, ProvinciaService],
101})
102export class AppModule {}
103
104console.log("\n=== STRUCTURE DU PROJET ===");
105console.log("LegionModule - gestion des légions (Controller + Service)");
106console.log("TributeService - gestion des tributs");
107console.log("ProvinciaService - gestion des provinces");
108console.log("AppModule - module principal qui relie tout");
109console.log("\nLe projet réunit les modules, l'injection de dépendances,");
110console.log("les contrôleurs et les services en une seule application.");
111Tu as repéré une erreur dans cette leçon ?