Skip to content

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 :

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
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] :

1
2
3
4
5
6
[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 :

Image

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/
1
2
3
4
5
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 :

1
2
3
4
5
6
7
8
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] :

1
2
3
4
5
6
7
8
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 :

Image

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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
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}

Image

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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
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
HTTP/1.1 204 No Content
curl -i http://localhost:3000/reponses/promesse
1
2
3
4
5
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
1
2
3
4
5
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 :

Image

Image

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
1
2
3
4
5
6
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
1
2
3
4
5
6
7
8
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
1
2
3
4
5
6
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 :

Image

curl -i http://localhost:3000/documentation
1
2
3
4
5
6
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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
6
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
1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
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
1
2
3
4
5
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.