À propos de ce cours
Objectifs de la leçon
- Classer les décorateurs NestJS par famille.
- Utiliser les décorateurs de route et de paramètre courants.
- Créer un décorateur de paramètre avec createParamDecorator.
- Attacher et lire des métadonnées avec SetMetadata et le Reflector.
- Combiner plusieurs décorateurs avec applyDecorators.
Les familles de décorateurs
Au chapitre 04, vous avez appris qu'un décorateur enregistre des métadonnées lues par le framework. NestJS en fournit plusieurs dizaines, que l'on peut ranger en cinq familles :
Classe, amélioration, injection, route et paramètre : chaque décorateur a sa place.
| Famille | Décorateurs | Paquet |
|---|---|---|
| Classe | @Module, @Controller, @Injectable, @Global, @Catch | @nestjs/common |
| Route (méthode HTTP) | @Get, @Post, @Put, @Patch, @Delete, @Options, @Head, @All | @nestjs/common |
| Route (réponse) | @HttpCode, @Header, @Redirect, @Render | @nestjs/common |
| Paramètre | @Param, @Query, @Body, @Headers, @Ip, @HostParam, @Req, @Res, @UploadedFile | @nestjs/common |
| Injection | @Inject, @Optional | @nestjs/common |
| Amélioration | @UseGuards, @UseInterceptors, @UsePipes, @UseFilters | @nestjs/common |
| Création | createParamDecorator, SetMetadata, applyDecorators, Reflector | @nestjs/common, @nestjs/core |
@Req() et @Res() donnent accès aux objets Express bruts. À éviter autant que possible : avec @Res(), NestJS ne gère plus la réponse (c'est à vous d'appeler res.json()), et les intercepteurs ne s'appliquent plus. Préférez renvoyer une valeur et utiliser @HttpCode / @Header.
Les décorateurs de route et de paramètre
Ils seront détaillés au chapitre 06. En voici l'essentiel, appliqué à un contrôleur de notes :
📄 src/notes.controller.ts
import {
Body, Controller, Delete, Get, Header, HttpCode, HttpStatus, Ip, Param, Post, Query, Redirect,
} from '@nestjs/common';
import { ReserveA, UtilisateurCourant } from './common/decorateurs';
@Controller('notes')
export class NotesController {
private notes = [{ id: 1, stagiaire: 'Ben Ali', valeur: 14 }];
@Get()
@Header('Cache-Control', 'no-store')
findAll(@Query('min') min?: string, @UtilisateurCourant('nom') nom?: string) {
const liste = this.notes.filter((n) => n.valeur >= Number(min ?? 0));
return { demandePar: nom, liste };
}
@Post()
@ReserveA('formateur', 'admin')
create(@Body() body: { stagiaire: string; valeur: number }, @UtilisateurCourant() auteur: object) {
const note = { id: this.notes.length + 1, ...body };
this.notes.push(note);
return { note, auteur };
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) // 204 : pas de corps
@ReserveA('admin')
remove(@Param('id') id: string) {
this.notes = this.notes.filter((n) => n.id !== Number(id));
}
@Get('aide')
@Redirect('https://docs.nestjs.com', 302)
aide() {}
@Get('client')
client(@Ip() ip: string) {
return { ip };
}
}
@Query('min')lit?min=10dans l'URL ;@Param('id')lit:id;@Body()lit le corps JSON.@Header('Cache-Control', 'no-store')ajoute un en-tête à la réponse.@HttpCode(HttpStatus.NO_CONTENT)remplace le statut par défaut (200, ou 201 pour POST).@Redirect(url, 302)renvoie une redirection.@UtilisateurCourantet@ReserveAsont des décorateurs personnalisés, écrits ci-dessous.
Créer un décorateur de paramètre : createParamDecorator
Dans ce contrôleur, plusieurs méthodes ont besoin de l'utilisateur qui fait la requête. Plutôt que de lire @Req() et de décoder l'en-tête dans chaque méthode, on crée un décorateur de paramètre. Pour l'exemple, l'utilisateur est transmis dans un en-tête X-Utilisateur: nom:role (au chapitre 13, il viendra d'un jeton JWT vérifié).
📄 src/common/decorateurs.ts
import {
applyDecorators, CanActivate, createParamDecorator, ExecutionContext, ForbiddenException,
Injectable, SetMetadata, UseGuards,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
// 1. Décorateur de paramètre : lit l'utilisateur dans l'en-tête X-Utilisateur
export const UtilisateurCourant = createParamDecorator(
(champ: 'nom' | 'role' | undefined, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest<Request>();
const [nom, role] = String(req.headers['x-utilisateur'] ?? 'anonyme:visiteur').split(':');
const utilisateur = { nom, role };
return champ ? utilisateur[champ] : utilisateur;
},
);
// 2. Métadonnée : rôles autorisés
export const ROLES = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES, roles);
// 3. Guard qui lit cette métadonnée avec le Reflector
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>(ROLES, ctx.getHandler());
if (!roles) return true;
const req = ctx.switchToHttp().getRequest<Request>();
const role = String(req.headers['x-utilisateur'] ?? '').split(':')[1];
if (!roles.includes(role)) throw new ForbiddenException(`Réservé à : ${roles.join(', ')}`);
return true;
}
}
// 4. Composition : un seul décorateur pour plusieurs
export const ReserveA = (...roles: string[]) => applyDecorators(Roles(...roles), UseGuards(RolesGuard));
createParamDecorator((data, ctx) => …):dataest l'argument passé au décorateur ('nom'dans@UtilisateurCourant('nom')),ctxdonne accès à la requête. La valeur renvoyée devient la valeur du paramètre.SetMetadata(clé, valeur)attache une métadonnée à la méthode ;Roles(...)n'est qu'un raccourci.- Le guard lit cette métadonnée avec le
Reflector(injecté par NestJS) etctx.getHandler()(la méthode visée). applyDecorators(...)combine plusieurs décorateurs en un seul :@ReserveA('admin')remplace@Roles('admin')+@UseGuards(RolesGuard).
Réponses réelles de l'application :
Requêtes envoyées (méthode, URL, en-tête X-Utilisateur) et réponses :
GET /notes?min=10 [sami:stagiaire] → 200 {"demandePar":"sami","liste":[{"id":1,"stagiaire":"Ben Ali","valeur":14}]} no-store
POST /notes [sami:stagiaire] → 403 {"message":"Réservé à : formateur, admin","error":"Forbidden","statusCode":403}
POST /notes [hajji:formateur] → 201 {"note":{"id":2,"stagiaire":"Gharbi","valeur":12},"auteur":{"nom":"hajji","role":"formateur"}}
DELETE /notes/1 [hajji:formateur] → 403 {"message":"Réservé à : admin","error":"Forbidden","statusCode":403}
DELETE /notes/1 [directeur:admin] → 204
GET /notes [-] → 200 {"demandePar":"anonyme","liste":[{"id":2,"stagiaire":"Gharbi","valeur":12}]} no-store
GET /notes/aide [-] → 302 Found. Redirecting to https://docs.nestjs.com
Sécurité: un en-tête comme X-Utilisateur peut être écrit par n'importe quel client : ce mécanisme sert uniquement à illustrer les décorateurs. L'authentification réelle (JWT signé et vérifié) est l'objet du chapitre 13, qui réutilisera exactement ce découpage : décorateur de paramètre, métadonnée de rôles, guard et Reflector.
Les décorateurs d'injection : @Inject et @Optional
@Injectable()
export class ExportService {
constructor(
@Inject(PARAMETRES_OPTIONS) private readonly options: ParametresOptions, // jeton (leçon 5.3)
@Optional() private readonly journal?: JournalService, // peut être absent
) {}
exporter() {
this.journal?.ecrire('Export', 'export lancé'); // ?. car le service est facultatif
return `Export ${this.options.etablissement}`;
}
}
Sans @Optional(), l'absence de JournalService empêcherait l'application de démarrer ; avec, NestJS injecte undefined.
Les décorateurs d'amélioration : où les placer ?
@Controller('notes')
@UseGuards(AuthGuard) // toutes les routes du contrôleur
export class NotesController {
@Post()
@UseGuards(RolesGuard) // en plus, pour cette route seulement
@UsePipes(new ValidationPipe())
@UseInterceptors(JournalInterceptor)
create(@Body() dto: CreerNoteDto) {}
}
// main.ts : pour toute l'application
app.useGlobalGuards(new AuthGuard());
Le même enrichisseur peut s'appliquer à trois niveaux : global, contrôleur ou route. La leçon suivante montre dans quel ordre ils s'exécutent.
Activités
Activité 1 Classer des décorateurs
Classez par famille : @Patch, @Optional, @Catch, @Headers, @UseFilters, @HttpCode, @Injectable, @Ip, @All, @Inject.
Voir le corrigé
Classe : @Catch, @Injectable. Route : @Patch, @HttpCode, @All. Paramètre : @Headers, @Ip. Injection : @Optional, @Inject. Amélioration : @UseFilters.
Activité 2 Un décorateur @Langue
Écrivez un décorateur de paramètre @Langue(parDefaut?) qui renvoie les deux premières lettres de l'en-tête Accept-Language si elles valent fr, ar ou en, sinon la langue par défaut ('fr' si rien n'est précisé). Utilisez-le dans une route GET /accueil qui renvoie un message de bienvenue dans la bonne langue.
Voir le corrigé
import { Controller, createParamDecorator, ExecutionContext, Get, Module } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Request } from 'express';
export const Langue = createParamDecorator((parDefaut: string | undefined, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest<Request>();
const valeur = req.headers['accept-language']?.split(',')[0]?.slice(0, 2);
return valeur && ['fr', 'ar', 'en'].includes(valeur) ? valeur : (parDefaut ?? 'fr');
});
const messages: Record<string, string> = { fr: 'Bienvenue', ar: 'مرحبا', en: 'Welcome' };
@Controller('accueil')
class AccueilController {
@Get()
accueil(@Langue('fr') langue: string) {
return { langue, message: messages[langue] };
}
}
Résultat :
Accept-Language: ar-TN,ar;q=0.9 → {"langue":"ar","message":"مرحبا"}
Accept-Language: en-US → {"langue":"en","message":"Welcome"}
Accept-Language: de-DE → {"langue":"fr","message":"Bienvenue"}
Accept-Language: (absent) → {"langue":"fr","message":"Bienvenue"}
Activité 3 Un décorateur @Public
On veut protéger toutes les routes par défaut et en ouvrir quelques-unes. Écrivez @Public() avec SetMetadata('estPublic', true), puis un guard qui laisse passer une route publique ou une requête portant l'en-tête X-Utilisateur, et refuse les autres. Indice : pour lire la métadonnée à la fois sur la méthode et sur la classe, utilisez reflector.getAllAndOverride('estPublic', [ctx.getHandler(), ctx.getClass()]).
Voir le corrigé
export const EST_PUBLIC = 'estPublic';
export const Public = () => SetMetadata(EST_PUBLIC, true);
@Injectable()
export class ConnexionGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const estPublic = this.reflector.getAllAndOverride<boolean>(EST_PUBLIC, [ctx.getHandler(), ctx.getClass()]);
if (estPublic) return true;
const req = ctx.switchToHttp().getRequest<Request>();
if (!req.headers['x-utilisateur']) throw new UnauthorizedException('Connexion requise');
return true;
}
}
// main.ts
app.useGlobalGuards(new ConnexionGuard(app.get(Reflector)));
// Dans un contrôleur
@Public()
@Get('catalogue')
catalogue() { … }
C'est exactement le schéma d'authentification « tout protégé sauf @Public » du chapitre 13. UnauthorizedException produit un 401 (« qui êtes-vous ? »), à distinguer du 403 du RolesGuard (« vous n'avez pas le droit »).
Activité 4 Composer avec applyDecorators
Écrivez un décorateur @RouteAdmin(chemin) qui équivaut à @Delete(chemin) + @HttpCode(204) + @Roles('admin') + @UseGuards(RolesGuard), et utilisez-le pour la suppression d'une note.
Voir le corrigé
export const RouteAdmin = (chemin: string) =>
applyDecorators(Delete(chemin), HttpCode(HttpStatus.NO_CONTENT), Roles('admin'), UseGuards(RolesGuard));
@Controller('notes')
export class NotesController {
@RouteAdmin(':id')
remove(@Param('id') id: string) { … }
}
Les décorateurs de route sont eux aussi composables. Attention à ne pas masquer l'essentiel : un décorateur composé doit avoir un nom explicite.
Activité 5 Repérer les mauvaises pratiques
Que reprochez-vous à ce code ?
@Get(':id')
findOne(@Req() req: Request, @Res() res: Response) {
const id = parseInt(req.params.id);
const role = (req.headers['x-utilisateur'] as string).split(':')[1];
if (role !== 'admin') return res.status(403).send('Interdit');
res.status(200).json(this.service.findOne(id));
}
Voir le corrigé
(1) @Req au lieu de @Param('id', ParseIntPipe): pas de validation, parseInt('abc') donne NaN. (2) @Res: NestJS ne gère plus la réponse, les intercepteurs et filtres ne s'appliquent plus. (3) Contrôle d'accès écrit dans la méthode au lieu d'un guard réutilisable. (4) Plantage si l'en-tête est absent (undefined.split). Version correcte : @Get(':id') @ReserveA('admin') findOne(@Param('id', ParseIntPipe) id: number) { return this.service.findOne(id); }.
À retenir
- Cinq familles : classe, route, paramètre, injection, amélioration.
- Préférer @Param/@Query/@Body à @Req, et une valeur renvoyée à @Res.
- createParamDecorator crée un paramètre personnalisé (ex. utilisateur courant).
- SetMetadata attache une donnée ; le Reflector la relit dans un guard ou un intercepteur.
- applyDecorators regroupe plusieurs décorateurs en un seul.
- @UseGuards, @UseInterceptors, @UsePipes, @UseFilters : au niveau route, contrôleur ou global.