2. Contrôleurs, actions, routage
Ce chapitre étudie le chemin d'une requête, du navigateur jusqu'à l'action qui la traite, et la réponse que renvoie cette action. Les vues n'apparaîtront qu'au chapitre 4 : ici, les actions renvoient directement du texte ou du JSON.
2.1. Exemple 01 : la structure d'une application NestJS
Le premier exemple est la plus petite application [NestJS] possible. Elle a trois fichiers :
| 01-structure/
├── src/
│ ├── app.controller.ts
│ ├── app.module.ts
│ └── main.ts
├── nest-cli.json
└── tsconfig.json
|
2.1.1. Le point d’entrée : main.ts
| import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// construit l'application à partir de son module racine
const app = await NestFactory.create(AppModule);
// démarre le serveur HTTP sur le port 3000
await app.listen(3000);
console.log('Serveur démarré sur http://localhost:3000');
}
bootstrap();
|
Commentons ce code :
- ligne 1 : [NestFactory] est la « fabrique » d'applications [NestJS] ;
- ligne 2 : le module racine de l'application, que nous verrons plus loin ;
- ligne 4 : la fonction de démarrage est asynchrone ([async]) : construire l'application et démarrer le serveur prennent du temps, et on attend ([await]) la fin de chaque étape ;
- ligne 6 : construit l'application à partir de son module racine. [NestJS] découvre alors tous les contrôleurs et services de l'application, et crée leurs instances ;
- ligne 8 : démarre le serveur HTTP sur le port 3000 ;
- ligne 12 : appel de la fonction de démarrage.
2.1.2. Le module racine : app.module.ts
| import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
controllers: [AppController],
})
export class AppModule {}
|
- ligne 4 : le décorateur [@Module] fait de la classe [AppModule] un module [NestJS]. Un module regroupe des contrôleurs et des services qui vont ensemble. Une petite application n'a qu'un module ; une grande en a plusieurs (cf. exemple 06) ;
- ligne 5 : la liste des contrôleurs du module ;
- ligne 7 : la classe elle-même est vide : tout est dans le décorateur.
2.1.3. Le contrôleur : app.controller.ts
| import { Controller, Get } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
accueil(): string {
return 'Bonjour de la part de NestJS !';
}
}
|
- ligne 3 : le décorateur [@Controller()] fait de la classe un contrôleur. Sans argument, ses routes commencent à la racine du site ([/]) ;
- ligne 5 : le décorateur [@Get()] associe la méthode [accueil] aux requêtes [GET /]. Une méthode ainsi décorée est une action ;
- ligne 7 : la valeur rendue par l'action est la réponse envoyée au navigateur.
2.1.4. Exécution
Dans une console ouverte sur le dossier [01-structure], on tape [npx nest start] :
| [Nest] 1885 - 09/23/2026, 4:02:05 PM LOG [NestFactory] Starting Nest application...
[Nest] 1885 - 09/23/2026, 4:02:05 PM LOG [InstanceLoader] AppModule dependencies initialized +8ms
[Nest] 1885 - 09/23/2026, 4:02:05 PM LOG [RoutesResolver] AppController {/}: +3ms
[Nest] 1885 - 09/23/2026, 4:02:05 PM LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 1885 - 09/23/2026, 4:02:05 PM LOG [NestApplication] Nest application successfully started +1ms
Serveur démarré sur http://localhost:3000
|
[NestJS] affiche les routes qu'il a trouvées : ici une seule, [{/, GET}], servie par [AppController]. Demandons l'URL [http://localhost:3000/] avec un navigateur :

puis avec [curl]. L'option [-i] affiche aussi les entêtes HTTP de la réponse (nous n'en gardons ici que quelques-uns) :
| curl -i http://localhost:3000/
|
| HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 30
Bonjour de la part de NestJS !
|
Une chaîne de caractères rendue par une action est envoyée avec l'entête [Content-Type: text/html] : le navigateur l'interprète comme du HTML.
2.2. Exemple 02 : le routage
Le routage associe une URL et une méthode HTTP (GET, POST...) à une action. L'exemple 02 a deux contrôleurs :
| 02-routage/
├── src/
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── main.ts
│ └── premier.controller.ts
├── nest-cli.json
└── tsconfig.json
|
Le fichier [src/app.module.ts] :
| import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { PremierController } from './premier.controller';
@Module({
controllers: [AppController, PremierController],
})
export class AppModule {}
|
ligne 6 : le module déclare ses deux contrôleurs.
Le fichier [src/app.controller.ts] :
| import { Controller, Get } from '@nestjs/common';
@Controller()
export class AppController {
// GET /
@Get()
accueil(): string {
return `<h2>Exemple 02 - le routage</h2>
<ul>
<li><a href="/premier">/premier</a></li>
<li><a href="/premier/action01">/premier/action01</a></li>
<li><a href="/premier/produits/17">/premier/produits/17</a></li>
<li><a href="/premier/produits/17/avis/3">/premier/produits/17/avis/3</a></li>
<li><a href="/premier/fichiers/images/logo.png">/premier/fichiers/images/logo.png</a></li>
<li><a href="/inconnue">/inconnue</a></li>
</ul>`;
}
}
|
L'action [accueil] (lignes 6-17) renvoie une page HTML qui contient des liens vers les actions de l'autre contrôleur :

Le fichier [src/premier.controller.ts] :
| import { Controller, Get, Param, Post } from '@nestjs/common';
@Controller('premier')
export class PremierController {
// GET /premier
@Get()
index(): string {
return 'Action [index] du contrôleur [premier]';
}
// GET /premier/action01
@Get('action01')
action01(): string {
return 'Action [action01] du contrôleur [premier], requête GET';
}
// POST /premier/action01 : même chemin, autre méthode HTTP
@Post('action01')
action01Post(): string {
return 'Action [action01Post] du contrôleur [premier], requête POST';
}
// GET /premier/produits/17 : [:id] est un paramètre de route
@Get('produits/:id')
produit(@Param('id') id: string): string {
return `Produit n° ${id}`;
}
// GET /premier/produits/17/avis/3 : deux paramètres de route
@Get('produits/:id/avis/:numero')
avis(@Param() params: { id: string; numero: string }): string {
return `Avis n° ${params.numero} du produit n° ${params.id}`;
}
// GET /premier/fichiers/... : [*] accepte n'importe quelle fin de chemin
@Get('fichiers/*')
fichier(@Param('0') chemin: string): string {
return `Fichier demandé : ${chemin}`;
}
}
|
Commentons ce code :
- ligne 3 : [@Controller('premier')] fixe le préfixe commun à toutes les routes du contrôleur : ses actions traitent des URL qui commencent par [/premier] ;
- lignes 6-7 : [@Get()] sans argument : l'action [index] traite [GET /premier] ;
- lignes 12-13 : l'action [action01] traite [GET /premier/action01]. L'URL est la concaténation du préfixe du contrôleur et du chemin de l'action ;
- lignes 18-19 : le même chemin, mais pour la méthode POST : [@Post] remplace [@Get]. Il existe aussi [@Put], [@Delete], [@Patch], [@All]... Le nom de la méthode [TypeScript] ([action01Post]) est libre : seuls les décorateurs comptent pour le routage ;
- lignes 24-25 : [:id] est un paramètre de route : ce segment de l'URL peut prendre n'importe quelle valeur. Le décorateur [@Param('id')] injecte sa valeur dans le paramètre [id] de l'action. Cette valeur est toujours une chaîne : pour [/premier/produits/17], [id] vaut ['17'], pas le nombre 17 ;
- lignes 30-31 : deux paramètres de route. [@Param()] sans argument injecte un objet qui les contient tous : [{ id: '17', numero: '3' }] ;
- lignes 36-37 : le caractère [*] accepte n'importe quelle fin d'URL, y compris des [/]. La partie correspondante est le paramètre nommé [0].
Au démarrage, [NestJS] affiche toutes les routes :
| [Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [NestFactory] Starting Nest application...
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [InstanceLoader] AppModule dependencies initialized +7ms
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [RoutesResolver] AppController {/}: +3ms
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [RoutesResolver] PremierController {/premier}: +0ms
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [RouterExplorer] Mapped {/premier, GET} route +1ms
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [RouterExplorer] Mapped {/premier/action01, GET} route +0ms
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [RouterExplorer] Mapped {/premier/action01, POST} route +0ms
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [RouterExplorer] Mapped {/premier/produits/:id, GET} route +1ms
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [RouterExplorer] Mapped {/premier/produits/:id/avis/:numero, GET} route +0ms
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [RouterExplorer] Mapped {/premier/fichiers/*, GET} route +0ms
[Nest] 2023 - 09/23/2026, 4:02:10 PM LOG [NestApplication] Nest application successfully started +2ms
Serveur démarré sur http://localhost:3000
|
Voici les réponses à quelques requêtes :
| curl -i http://localhost:3000/premier
|
| HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 39
Action [index] du contrôleur [premier]
|
| curl -i http://localhost:3000/premier/action01
|
| HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 56
Action [action01] du contrôleur [premier], requête GET
|
| curl -i http://localhost:3000/premier/produits/17
|
| HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 14
Produit n° 17
|
| curl -i http://localhost:3000/premier/produits/17/avis/3
|
| HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 28
Avis n° 3 du produit n° 17
|
| curl -i http://localhost:3000/premier/fichiers/images/logo.png
|
| HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 34
Fichier demandé : images/logo.png
|
Pour envoyer une requête POST, on utilise l'option [-X POST] de [curl] :
| curl -i -X POST http://localhost:3000/premier/action01
|
| HTTP/1.1 201 Created
Content-Type: text/html; charset=utf-8
Content-Length: 61
Action [action01Post] du contrôleur [premier], requête POST
|
On remarquera le code de statut 201 Created : c'est le code que [NestJS] utilise par défaut pour répondre à un POST. Nous verrons comment le changer. Enfin, une URL qu'aucune route ne traite produit une erreur 404, que [NestJS] décrit par défaut en JSON :
| curl -i http://localhost:3000/inconnue
|
| HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Content-Length: 71
{"message":"Cannot GET /inconnue","error":"Not Found","statusCode":404}
|

Le chapitre 7 montrera comment remplacer ce JSON par une page HTML.
2.3. Exemple 03 : les réponses d’une action
Ce que renvoie une action détermine la réponse HTTP. L'exemple 03 passe en revue les principaux cas :
Le fichier [src/reponses.controller.ts] :
| import { Controller, Get, Header, HttpCode, NotFoundException } from '@nestjs/common';
@Controller('reponses')
export class ReponsesController {
// une chaîne : envoyée telle quelle, avec le type [text/html]
@Get('html')
html(): string {
return '<h1>Action [html]</h1>';
}
// la même chaîne, mais annoncée comme du texte brut
@Get('texte')
@Header('Content-Type', 'text/plain; charset=utf-8')
texte(): string {
return '<h1>Action [texte]</h1>';
}
// un objet : sérialisé en JSON, avec le type [application/json]
@Get('json')
json(): object {
return { nom: 'Martin', prenom: 'Jean', age: 42 };
}
// un tableau : lui aussi sérialisé en JSON
@Get('tableau')
tableau(): string[] {
return ['Mme', 'Mlle', 'Mr'];
}
// un code de statut choisi : 201 (Created) au lieu de 200 (OK)
@Get('cree')
@HttpCode(201)
cree(): object {
return { message: 'ressource créée' };
}
// aucune réponse : le code 204 (No Content) signifie "rien à afficher"
@Get('vide')
@HttpCode(204)
vide(): void {}
// une méthode asynchrone : NestJS attend la fin de la promesse
@Get('promesse')
async promesse(): Promise<object> {
const debut = Date.now();
await new Promise((fini) => setTimeout(fini, 500));
return { attente: `${Date.now() - debut} ms` };
}
// une exception : NestJS la transforme en réponse d'erreur
@Get('erreur')
erreur(): string {
throw new NotFoundException('Cette ressource n\'existe pas');
}
}
|
Commentons ce code :
- lignes 6-9 : une chaîne est envoyée telle quelle, avec l'entête [Content-Type: text/html; charset=utf-8] ;
- lignes 12-16 : le décorateur [@Header] fixe un entête de la réponse. Annoncée comme du texte brut ([text/plain]), la même chaîne n'est plus interprétée par le navigateur ;
- lignes 19-22 : un objet est sérialisé en JSON, avec l'entête [Content-Type: application/json] ;
- lignes 25-28 : un tableau est lui aussi sérialisé en JSON ;
- lignes 31-35 : [@HttpCode(201)] change le code de statut de la réponse (200 par défaut pour un GET) ;
- lignes 38-40 : le code 204 (No Content) signifie « pas de contenu » : l'action ne rend rien ;
- lignes 43-48 : une action peut être asynchrone ([async]) : elle rend alors une promesse ([Promise]), et [NestJS] attend sa résolution avant de répondre. C'est le cas de toutes les actions qui interrogent une base de données ;
- lignes 51-54 : une action peut lever une exception. Les exceptions de [NestJS] ([NotFoundException], [BadRequestException], [ForbiddenException]...) portent chacune un code de statut HTTP (ici 404), que [NestJS] utilise pour sa réponse.
Les réponses obtenues :
| curl -i http://localhost:3000/reponses/html
|
| HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 22
<h1>Action [html]</h1>
|
| curl -i http://localhost:3000/reponses/texte
|
| HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 23
<h1>Action [texte]</h1>
|
| curl -i http://localhost:3000/reponses/json
|
| HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 41
{"nom":"Martin","prenom":"Jean","age":42}
|
| curl -i http://localhost:3000/reponses/tableau
|
| HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 19
["Mme","Mlle","Mr"]
|
| curl -i http://localhost:3000/reponses/cree
|
| HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 31
{"message":"ressource créée"}
|
| curl -i http://localhost:3000/reponses/vide
|
| curl -i http://localhost:3000/reponses/promesse
|
| HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 20
{"attente":"500 ms"}
|
| curl -i http://localhost:3000/reponses/erreur
|
| HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Content-Length: 79
{"message":"Cette ressource n'existe pas","error":"Not Found","statusCode":404}
|
Dans un navigateur, la différence entre [text/html] et [text/plain] est visible :


2.4. Exemple 04 : les redirections
Une action peut répondre par une redirection : un code 301 ou 302 et un entête [Location] qui indique au navigateur l'URL quelle URL demander ensuite. Le navigateur la demande aussitôt, sans intervention de l'utilisateur. Les redirections sont omniprésentes dans une application web : après un formulaire réussi, après une connexion, après un changement de langue...
Le fichier [src/redirections.controller.ts] :
| import { Controller, Get, Param, Redirect } from '@nestjs/common';
@Controller()
export class RedirectionsController {
// redirection fixe : l'adresse et le code sont connus à l'avance
@Get('ancienne')
@Redirect('/nouvelle', 301)
ancienne(): void {}
@Get('nouvelle')
nouvelle(): string {
return 'Vous êtes sur la page [nouvelle]';
}
// redirection calculée : l'objet rendu remplace l'adresse de @Redirect()
@Get('aller/:destination')
@Redirect()
aller(@Param('destination') destination: string): { url: string } {
return { url: `/page/${destination}` };
}
@Get('page/:nom')
page(@Param('nom') nom: string): string {
return `Vous êtes sur la page [${nom}]`;
}
// redirection vers un autre site
@Get('documentation')
@Redirect('https://docs.nestjs.com')
documentation(): void {}
}
|
Commentons ce code :
- lignes 6-8 : [@Redirect('/nouvelle', 301)] : une redirection fixe. Le code 301 (Moved Permanently) indique que la page a définitivement changé d'adresse ; le code par défaut est 302 (Found), une redirection temporaire ;
- lignes 16-20 : une redirection calculée : [@Redirect()] sans argument, et l'action rend un objet [{ url: ... }] qui fixe l'adresse de destination ;
- lignes 28-30 : on peut rediriger vers un autre site.
[curl] montre la redirection sans la suivre :
| curl -i http://localhost:3000/ancienne
|
| HTTP/1.1 301 Moved Permanently
Location: /nouvelle
Content-Type: text/plain; charset=utf-8
Content-Length: 43
Moved Permanently. Redirecting to /nouvelle
|
Avec l'option [-L], [curl] suit la redirection, comme le ferait un navigateur. On voit alors les deux réponses successives :
| curl -i -L http://localhost:3000/ancienne
|
| HTTP/1.1 301 Moved Permanently
Location: /nouvelle
Content-Type: text/plain; charset=utf-8
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Vous êtes sur la page [nouvelle]
|
| curl -i http://localhost:3000/aller/contact
|
| HTTP/1.1 302 Found
Location: /page/contact
Content-Type: text/plain; charset=utf-8
Content-Length: 35
Found. Redirecting to /page/contact
|
Dans le navigateur, on ne voit que la page finale ; la barre d'adresse affiche la nouvelle URL :

| curl -i http://localhost:3000/documentation
|
| HTTP/1.1 302 Found
Location: https://docs.nestjs.com
Content-Type: text/plain; charset=utf-8
Content-Length: 45
Found. Redirecting to https://docs.nestjs.com
|
2.5. Exemple 05 : les objets Request et Response d’Express
[NestJS] s'appuie sur le serveur web [Express]. Une action peut accéder directement aux objets [Request] et [Response] d'[Express]. C'est parfois nécessaire, et nous le ferons dans l'étude de cas (pour déposer un cookie, par exemple), mais il faut savoir ce que l'on perd.
Le fichier [src/express.controller.ts] :
| import { Controller, Get, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
@Controller('express')
export class ExpressController {
// la requête Express : méthode, URL, entêtes...
@Get('requete')
requete(@Req() req: Request): object {
return {
methode: req.method,
url: req.url,
chemin: req.path,
navigateur: req.get('user-agent'),
};
}
// la réponse Express : c'est l'action qui envoie elle-même la réponse
@Get('reponse')
reponse(@Res() res: Response): void {
res.status(200).type('text/plain').send('Réponse envoyée par [res.send]');
}
// redirection avec la réponse Express
@Get('redirection')
redirection(@Res() res: Response): void {
res.redirect('/express/reponse');
}
// passthrough : on utilise la réponse (ici pour un entête), mais NestJS
// envoie toujours lui-même la valeur rendue par l'action
@Get('passthrough')
passthrough(@Res({ passthrough: true }) res: Response): string {
res.setHeader('X-Exemple', '05-express');
return 'Réponse envoyée par NestJS, avec un entête ajouté';
}
// ERREUR à ne pas commettre : @Res() sans passthrough, et rien d'envoyé
@Get('bloquee')
bloquee(@Res() res: Response): string {
return 'Cette chaîne ne sera jamais envoyée : le navigateur attend...';
}
}
|
Commentons ce code :
- ligne 2 : les types [Request] et [Response] d'[Express] ;
- lignes 7-15 : [@Req()] injecte la requête [Express] : méthode HTTP, URL, chemin, entêtes ([req.get(...)])... ;
- lignes 18-21 : [@Res()] injecte la réponse [Express]. Attention : dès qu'une action utilise [@Res()], [NestJS] considère que c'est elle qui envoie la réponse, et ignore ce qu'elle rend. Ici l'action fixe le code de statut, le type, et envoie le corps avec [res.send] ;
- lignes 24-27 : une redirection (302) faite par [Express] : [res.redirect(url)] ;
- lignes 31-35 : avec l'option [passthrough: true], l'action utilise la réponse (ici pour ajouter un entête) mais laisse [NestJS] envoyer ce qu'elle rend. C'est le meilleur des deux mondes ;
- lignes 38-41 : l'erreur à ne pas commettre : [@Res()] sans [passthrough], et aucun appel à [res.send], [res.redirect], [res.render]... La chaîne rendue est ignorée, aucune réponse n'est jamais envoyée, et le navigateur attend indéfiniment.
Les réponses obtenues :
| curl -i -A "Mozilla/5.0 (exemple)" http://localhost:3000/express/requete
|
| HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 107
{"methode":"GET","url":"/express/requete","chemin":"/express/requete","navigateur":"Mozilla/5.0 (exemple)"}
|
(l'option [-A] de [curl] fixe l'entête [User-Agent], qui identifie le navigateur)
| curl -i http://localhost:3000/express/reponse
|
| HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 32
Réponse envoyée par [res.send]
|
| curl -i http://localhost:3000/express/redirection
|
| HTTP/1.1 302 Found
Location: /express/reponse
Content-Type: text/plain; charset=utf-8
Content-Length: 38
Found. Redirecting to /express/reponse
|
| curl -i http://localhost:3000/express/passthrough
|
| HTTP/1.1 200 OK
X-Exemple: 05-express
Content-Type: text/html; charset=utf-8
Content-Length: 53
Réponse envoyée par NestJS, avec un entête ajouté
|
Pour la dernière action, on limite l'attente à 3 secondes avec l'option [-m 3] :
| curl -i -m 3 http://localhost:3000/express/bloquee
|
| curl: (28) Operation timed out after 3002 milliseconds with 0 bytes received
|
Dans la suite du document, nous utiliserons [@Res()] quand une action doit choisir entre plusieurs réponses de natures différentes (une page ou une redirection), et nous veillerons à ce que chaque chemin du code envoie une réponse.
2.6. Exemple 06 : modules, services et injection de dépendances
Les contrôleurs ne doivent pas contenir la logique de l'application : ils la délèguent à des services. L'exemple 06 découpe une petite application en deux modules :
| 06-modules/
├── src/
│ ├── clients/
│ │ ├── clients.controller.ts
│ │ └── clients.module.ts
│ ├── produits/
│ │ ├── produits.controller.ts
│ │ ├── produits.module.ts
│ │ └── produits.service.ts
│ ├── app.module.ts
│ └── main.ts
├── nest-cli.json
└── tsconfig.json
|
2.6.1. Le service ProduitsService
| import { Injectable } from '@nestjs/common';
export interface Produit {
id: number;
nom: string;
prix: number;
}
@Injectable()
export class ProduitsService {
private readonly produits: Produit[] = [
{ id: 1, nom: 'stylo', prix: 1.5 },
{ id: 2, nom: 'cahier', prix: 3.2 },
{ id: 3, nom: 'gomme', prix: 0.8 },
];
findAll(): Produit[] {
return this.produits;
}
findById(id: number): Produit | undefined {
return this.produits.find((p) => p.id === id);
}
}
|
- lignes 3-7 : l'interface [Produit] décrit un produit ;
- ligne 9 : le décorateur [@Injectable()] fait de la classe un service (on dit aussi un provider) : [NestJS] en crée une seule instance, qu'il fournit (injecte) à tous ceux qui en ont besoin ;
- lignes 11-15 : les données, ici en mémoire ;
- lignes 17-23 : les deux méthodes du service.
2.6.2. Le contrôleur ProduitsController
| import { Controller, Get, NotFoundException, Param } from '@nestjs/common';
import { Produit, ProduitsService } from './produits.service';
@Controller('produits')
export class ProduitsController {
// injection de dépendance : NestJS fournit l'instance de ProduitsService
constructor(private readonly produitsService: ProduitsService) {}
@Get()
liste(): Produit[] {
return this.produitsService.findAll();
}
@Get(':id')
unProduit(@Param('id') id: string): Produit {
const produit = this.produitsService.findById(Number(id));
if (!produit) {
throw new NotFoundException(`Produit n° ${id} introuvable`);
}
return produit;
}
}
|
- ligne 7 : l'injection de dépendances. Le contrôleur déclare dans son constructeur qu'il a besoin d'un [ProduitsService] ; [NestJS] lui fournit l'instance unique du service. Le contrôleur ne fait jamais [new ProduitsService()]. La syntaxe [private readonly produitsService: ProduitsService] de [TypeScript] déclare en même temps le paramètre et le champ [this.produitsService] ;
- lignes 9-12 : l'action délègue au service ;
- lignes 14-21 : [Number(id)] convertit le paramètre de route (une chaîne) en nombre. Un produit inexistant provoque une exception 404.
2.6.3. Le module ProduitsModule
| import { Module } from '@nestjs/common';
import { ProduitsController } from './produits.controller';
import { ProduitsService } from './produits.service';
@Module({
controllers: [ProduitsController],
providers: [ProduitsService],
exports: [ProduitsService],
})
export class ProduitsModule {}
|
- ligne 6 : le contrôleur du module ;
- ligne 7 : les services du module. Seuls les services déclarés ici (ou importés, cf. plus bas) peuvent être injectés dans les classes du module ;
- ligne 8 : le service est exporté : les modules qui importent [ProduitsModule] pourront, eux aussi, se le faire injecter.
2.6.4. Le module ClientsModule
| import { Module } from '@nestjs/common';
import { ClientsController } from './clients.controller';
import { ProduitsModule } from '../produits/produits.module';
@Module({
imports: [ProduitsModule],
controllers: [ClientsController],
})
export class ClientsModule {}
|
ligne 6 : [ClientsModule] importe [ProduitsModule], et a donc accès aux services que celui-ci exporte.
Le fichier [src/clients/clients.controller.ts] :
| import { Controller, Get } from '@nestjs/common';
import { ProduitsService } from '../produits/produits.service';
@Controller('clients')
export class ClientsController {
// un service d'un AUTRE module, exporté par ce module
constructor(private readonly produitsService: ProduitsService) {}
@Get('panier')
panier(): object {
const produits = this.produitsService.findAll();
const total = produits.reduce((somme, p) => somme + p.prix, 0);
return { client: 'Jean Martin', articles: produits.length, total };
}
}
|
- ligne 7 : [ClientsController] se fait injecter le service d'un autre module ;
- lignes 9-14 : il l'utilise pour calculer le total d'un panier.
2.6.5. Le module racine
| import { Module } from '@nestjs/common';
import { ProduitsModule } from './produits/produits.module';
import { ClientsModule } from './clients/clients.module';
@Module({
imports: [ProduitsModule, ClientsModule],
})
export class AppModule {}
|
ligne 6 : le module racine n'a plus de contrôleur : il se contente d'importer les deux modules de l'application.
2.6.6. Exécution
| [Nest] 2932 - 09/23/2026, 4:02:35 PM LOG [NestFactory] Starting Nest application...
[Nest] 2932 - 09/23/2026, 4:02:35 PM LOG [InstanceLoader] AppModule dependencies initialized +8ms
[Nest] 2932 - 09/23/2026, 4:02:35 PM LOG [InstanceLoader] ProduitsModule dependencies initialized +1ms
[Nest] 2932 - 09/23/2026, 4:02:35 PM LOG [InstanceLoader] ClientsModule dependencies initialized +0ms
[Nest] 2932 - 09/23/2026, 4:02:35 PM LOG [RoutesResolver] ProduitsController {/produits}: +3ms
[Nest] 2932 - 09/23/2026, 4:02:35 PM LOG [RouterExplorer] Mapped {/produits, GET} route +2ms
[Nest] 2932 - 09/23/2026, 4:02:35 PM LOG [RouterExplorer] Mapped {/produits/:id, GET} route +0ms
[Nest] 2932 - 09/23/2026, 4:02:35 PM LOG [RoutesResolver] ClientsController {/clients}: +1ms
[Nest] 2932 - 09/23/2026, 4:02:35 PM LOG [RouterExplorer] Mapped {/clients/panier, GET} route +0ms
[Nest] 2932 - 09/23/2026, 4:02:35 PM LOG [NestApplication] Nest application successfully started +3ms
Serveur démarré sur http://localhost:3000
|
| curl -i http://localhost:3000/produits
|
| HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 104
[{"id":1,"nom":"stylo","prix":1.5},{"id":2,"nom":"cahier","prix":3.2},{"id":3,"nom":"gomme","prix":0.8}]
|
| curl -i http://localhost:3000/produits/2
|
| HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 34
{"id":2,"nom":"cahier","prix":3.2}
|
| curl -i http://localhost:3000/produits/9
|
| HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Content-Length: 76
{"message":"Produit n° 9 introuvable","error":"Not Found","statusCode":404}
|
| curl -i http://localhost:3000/clients/panier
|
| HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 49
{"client":"Jean Martin","articles":3,"total":5.5}
|
L'étude de cas est construite exactement ainsi : des contrôleurs minces, qui délèguent à des services injectés, et des modules qui regroupent ce qui va ensemble.