Cours NestJS · Module 2: Routing et cycle de requête
Décorateurs personnalisés - sceaux et insignes de l'empire
Dans cette leçon6
Le sénateur Cicéron a parcouru les contrôleurs de l'empire et a froncé les sourcils. Chaque méthode répétait la même ligne const user = req.user, chaque endpoint portait les quatre mêmes décorateurs, et la liste des rôles était recopiée à la main à dix endroits. Une seule faute de frappe et le trésor reste grand ouvert. Rome réglait ce problème avec des insignes : l'anneau d'un sénateur ou la toge d'un préteur disaient au premier coup d'œil qui était leur porteur et ce qu'il avait le droit de faire. Dans NestJS, ce rôle revient aux décorateurs personnalisés - tes propres sceaux, que tu attaches à une méthode, à une classe ou à un paramètre.
L'anneau du sénateur : un décorateur de paramètre
Le décorateur que tu écriras le plus souvent est un décorateur de paramètre. Il extrait des données de la requête et les passe directement à un argument de la méthode. On le crée avec createParamDecorator, qui reçoit une fabrique à deux arguments : data, la valeur écrite entre les parenthèses du décorateur, et ExecutionContext, que tu connais déjà depuis la leçon sur les guards :
1// decorators/current-user.decorator.ts
2import { createParamDecorator, ExecutionContext } from '@nestjs/common';
3
4// @CurrentUser() decorator - extracts user data from the request
5export const CurrentUser = createParamDecorator(
6 (data: string, ctx: ExecutionContext) => {
7 const request = ctx.switchToHttp().getRequest();
8 const user = request.user; // Défini plus tôt par AuthGuard
9
10 // If a specific field is provided, return only that field
11 return data ? user?.[data] : user;
12 },
13);La fabrique s'exécute à chaque requête, et NestJS place son résultat à l'endroit du paramètre. Le décorateur ne lit rien dans la base de données et ne modifie pas la requête - il se contente de lire request.user, défini plus tôt par un guard d'authentification. L'opérateur ?. évite une erreur quand il n'y a pas d'utilisateur.
Voici son utilisation dans le contrôleur de la légion :
1// Utilisation dans le contrôleur
2@Controller('legiones')
3export class LegionController {
4 @Get('profile')
5 @UseGuards(JwtAuthGuard)
6 getProfile(@CurrentUser() user: any) {
7 // user = { id: 1, name: 'Marcus', rank: 'Centurio', legion: 'Legio X' }
8 return { message: `Ave, ${user.name}!`, profile: user };
9 }
10
11 @Get('rank')
12 @UseGuards(JwtAuthGuard)
13 getRank(@CurrentUser('rank') rank: string) {
14 // rank = 'Centurio' (only the specific field)
15 return { rank };
16 }
17}Écrire @CurrentUser('rank') signifie que data === 'rank', donc la fabrique renvoie seulement user.rank. Une remarque pratique : par défaut, ValidationPipe ne valide pas les paramètres issus de décorateurs personnalisés, tant que tu n'actives pas l'option validateCustomDecorators: true.
Un édit impérial : SetMetadata
Le deuxième outil, ce sont les métadonnées, des étiquettes attachées à une méthode ou à une classe. SetMetadata(clé, valeur) enregistre une telle étiquette, et nous l'enveloppons dans notre propre décorateur pour ne pas répéter la clé dans chaque contrôleur :
1// decorators/roles.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4// Simple @Roles() decorator setting metadata
5export const Roles = (...roles: string[]) => SetMetadata('roles', roles);Le paramètre du reste ...roles rassemble tous les rôles fournis dans un tableau. Attachons maintenant l'édit aux méthodes :
1// Usage
2@Controller('tributa')
3export class TributeController {
4 @Post()
5 @Roles('consul', 'praetor') // Seuls le consul et le préteur peuvent créer
6 createTribute(@Body() dto: any) {
7 return { message: 'Tribut créé !' };
8 }
9
10 @Delete(':id')
11 @Roles('consul') // Seul le consul peut supprimer
12 removeTribute(@Param('id') id: string) {
13 return { message: `Tribute ${id} removed` };
14 }
15}Attention, c'est le malentendu le plus courant : les métadonnées seules ne bloquent rien. C'est un édit affiché sur le forum - il ne s'applique que lorsque quelqu'un le lit et le fait respecter.
Le prétorien lit l'édit : Reflector dans un guard
Ce lecteur, c'est un guard. Reflector, du paquet @nestjs/core, lit les métadonnées, et context.getHandler() désigne la méthode qui va traiter la requête :
1// guards/roles.guard.ts
2import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
3import { Reflector } from '@nestjs/core';
4
5@Injectable()
6export class RolesGuard implements CanActivate {
7 constructor(private reflector: Reflector) {}
8
9 canActivate(context: ExecutionContext): boolean {
10 // Read 'roles' metadata from the handler
11 const requiredRoles = this.reflector.get<string[]>(
12 'roles',
13 context.getHandler(),
14 );
15
16 // No required roles (or an empty list) = access for every signed-in user
17 if (!requiredRoles?.length) return true;
18
19 const request = context.switchToHttp().getRequest();
20 const user = request.user;
21
22 // Vérifie si l'utilisateur a au moins un des rôles requis
23 return requiredRoles.some(role => user?.roles?.includes(role));
24 }
25}Pas d'étiquette signifie pas d'exigence, donc le guard laisse passer la requête. La condition !requiredRoles?.length couvre aussi une liste vide - sans elle, un décorateur sans rôles bloquerait tout le monde, car [].some() renvoie false. Souviens-toi aussi que get() ne lit les métadonnées que sur la cible que tu lui donnes - ici, la méthode (context.getHandler()). Si tu places aussi @Roles() sur la classe, utilise this.reflector.getAllAndOverride('roles', [context.getHandler(), context.getClass()]) - sinon le rôle de la classe est ignoré sans bruit.
La documentation de NestJS qualifie SetMetadata d'approche de bas niveau et présente une variante typée :
1// decorators/roles.decorator.ts - variant with Reflector.createDecorator
2import { Reflector } from '@nestjs/core';
3
4export const Roles = Reflector.createDecorator<string[]>();
5// usage: @Roles(['consul', 'praetor'])
6// reading: this.reflector.get(Roles, context.getHandler())Ici, la clé est le décorateur lui-même : une faute de frappe dans le nom de la clé devient impossible, et TypeScript contrôle le type de la valeur. Dans du code neuf, c'est la variante que je choisis. SetMetadata fonctionne toujours, et tu le croiseras dans bien des projets.
Un marqueur public : @Public()
Un décorateur n'a pas besoin d'en combiner plusieurs autres. Celui-ci pose une seule étiquette et exporte la clé sous forme de constante, pour qu'un guard puisse la lire sans retaper la chaîne :
1// decorators/public.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4// Décorateur qui marque un endpoint comme public (sans autorisation)
5export const IS_PUBLIC_KEY = 'isPublic';
6export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
7
8// Usage
9@Controller('forum')
10export class ForumController {
11 @Get()
12 @Public() // Cet endpoint est public
13 getPublicPosts() {
14 return { posts: ['Message du Forum Romanum'] };
15 }
16}Le marqueur seul n'ouvre rien. Il fonctionne avec un guard d'authentification global qui vérifie d'abord getAllAndOverride(IS_PUBLIC_KEY, ...) et, si la valeur vaut true, laisse passer la requête sans token.
Le grand sceau : applyDecorators
Quand tu répètes le même ensemble de décorateurs au-dessus de tes endpoints, applyDecorators les réunit en un seul :
1// decorators/auth.decorator.ts
2import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';
3import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger';
4import { JwtAuthGuard } from '../guards/jwt-auth.guard';
5import { RolesGuard } from '../guards/roles.guard';
6
7// Composite @Auth() decorator combining guard, roles, and documentation
8export function Auth(...roles: string[]) {
9 return applyDecorators(
10 SetMetadata('roles', roles),
11 UseGuards(JwtAuthGuard, RolesGuard),
12 ApiBearerAuth(),
13 ApiUnauthorizedResponse({ description: 'Unauthorized' }),
14 );
15}À l'intérieur se trouvent quatre décorateurs ordinaires : les métadonnées des rôles, deux guards et les descriptions pour Swagger. applyDecorators n'ajoute aucune logique - il les applique simplement tous d'un coup.
1// Usage - one decorator instead of four!
2@Controller('imperium')
3export class ImperiumController {
4 @Post('decree')
5 @Auth('consul', 'praetor') // Authorization + roles + Swagger
6 issueDecree(@Body() decree: any) {
7 return { message: 'Décret promulgué !', decree };
8 }
9
10 @Get('treasury')
11 @Auth('consul', 'quaestor') // Seuls le consul et le questeur
12 getTreasury() {
13 return { treasury: "État du trésor de l'empire" };
14 }
15}Une ligne en remplace quatre, et les guards fonctionnent exactement comme avant. Modifier l'ensemble dans la fonction Auth met à jour tout le projet d'un seul coup.
Applications pratiques
Pour finir, deux autres décorateurs de métadonnées qui, avec @Auth() et @CurrentUser(), décrivent le recrutement d'un légionnaire :
1// decorators/log-action.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4// Décorateur pour journaliser les actions dans les chroniques de l'empire
5export const LOG_ACTION_KEY = 'logAction';
6export const LogAction = (actionName: string) =>
7 SetMetadata(LOG_ACTION_KEY, actionName);
8
9// decorators/rate-limit.decorator.ts
10export const RATE_LIMIT_KEY = 'rateLimit';
11export const RateLimit = (maxRequests: number, windowMs: number) =>
12 SetMetadata(RATE_LIMIT_KEY, { maxRequests, windowMs });
13
14// Utilisation dans le contrôleur
15@Controller('legiones')
16export class LegionController {
17 @Post('recruit')
18 @Auth('consul')
19 @LogAction('RECRUIT_LEGIONARY')
20 @RateLimit(10, 60000) // Max 10 recruitments per minute
21 recruitLegionary(@Body() data: any, @CurrentUser() user: any) {
22 return {
23 message: `${user.name} a recruté un nouveau légionnaire`,
24 legionary: data,
25 };
26 }
27}Souviens-toi de la règle de l'édit : @LogAction() ne fonctionne qu'avec un intercepteur qui lit la clé et écrit une entrée dans la chronique, et @RateLimit() qu'avec un guard qui compte les requêtes. Pour limiter le débit, je te recommande le paquet tout prêt @nestjs/throttler et son décorateur @Throttle({ default: { limit: 10, ttl: 60000 } }), plutôt que de construire un compteur de zéro.
Dans la prochaine leçon, tu construiras les tribunaux, c'est-à-dire les exception filters, et dans le module consacré à l'authentification, @CurrentUser() recevra l'utilisateur préparé par la stratégie JWT.
Souviens-toi : un décorateur de paramètre exécute sa fabrique à chaque requête, tandis qu'un décorateur de métadonnées n'est qu'un sceau qui agit une fois qu'un guard ou un intercepteur le lit.
Code de cette leçon : src/custom-decorators.ts
1// Décorateurs personnalisés - Sceaux et insignes de l'empire
2import { createParamDecorator, ExecutionContext, SetMetadata, applyDecorators, UseGuards } from '@nestjs/common';
3
4console.log("Décorateurs personnalisés - crée tes propres sceaux de l'empire !");
5
6// ===========================================
7// 1. Décorateur de paramètre personnalisé
8// ===========================================
9
10const CurrentUser = createParamDecorator(
11 (data: string, ctx: ExecutionContext) => {
12 const request = ctx.switchToHttp().getRequest();
13 const user = request.user;
14 return data ? user?.[data] : user;
15 },
16);
17
18console.log("@CurrentUser() - extrait les données de l'utilisateur de la requête");
19console.log("@CurrentUser('rank') - extrait un champ précis");
20
21// ===========================================
22// 2. SetMetadata et Roles
23// ===========================================
24
25const Roles = (...roles: string[]) => SetMetadata('roles', roles);
26
27console.log("\n@Roles('consul', 'praetor') - définit les rôles requis");
28console.log("Reflector.get('roles', handler) - lit les rôles dans le guard");
29
30// ===========================================
31// 3. applyDecorators - combiner les décorateurs
32// ===========================================
33
34function Auth(...roles: string[]) {
35 return applyDecorators(
36 SetMetadata('roles', roles),
37 // UseGuards(JwtAuthGuard, RolesGuard),
38 );
39}
40
41console.log("\napplyDecorators() - combine plusieurs décorateurs en un seul");
42console.log("@Auth('consul') = SetMetadata('roles', ...) (dans la leçon aussi UseGuards et ApiBearerAuth)");
43
44// ===========================================
45// 4. Exemple pratique
46// ===========================================
47
48const IS_PUBLIC_KEY = 'isPublic';
49const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
50
51const LOG_ACTION_KEY = 'logAction';
52const LogAction = (actionName: string) => SetMetadata(LOG_ACTION_KEY, actionName);
53
54console.log("\n=== RÉSUMÉ SUR LES DÉCORATEURS PERSONNALISÉS ===");
55console.log("createParamDecorator - décorateur de paramètre");
56console.log("SetMetadata - affectation de métadonnées");
57console.log("Reflector - lecture des métadonnées dans un guard/intercepteur");
58console.log("applyDecorators - combinaison de plusieurs décorateurs");
59Tu 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. Quelle fonction de NestJS utilise-t-on pour créer un décorateur de paramètre personnalisé (par ex. @CurrentUser()) ?
2. Que représente l'argument 'data' dans createParamDecorator((data, ctx) => ...) ? Par ex. lors de l'utilisation de @CurrentUser('rank').
3. À quoi sert la fonction SetMetadata() dans NestJS ?
4. Que fait la fonction applyDecorators() dans NestJS ?
Tâches pratiques dans le jeu
- Éditeur de code
Écris un décorateur @CurrentUser() utilisant createParamDecorator qui retourne request.user ou un champ spécifique user[data]
- Clique dans l'ordre
Range les éléments de la création d'un décorateur de paramètre personnalisé dans le bon ordre :
- Éditeur de code
Écris un décorateur @Roles() utilisant SetMetadata('roles', roles) et un RolesGuard avec Reflector qui vérifie les rôles de l'utilisateur
- Tri horizontal
Range les éléments d'un appel à SetMetadata pour définir les rôles requis :
- Tri vertical
Ordonne les lignes du décorateur @Auth() de la leçon de haut en bas :