Kurs NestJS · Moduł 2: Routing i cykl żądania
Własne dekoratory - pieczęcie i insygnia imperium
W tej lekcji6
Senator Cicero przejrzał kontrolery imperium i zmarszczył brwi. W każdej metodzie ta sama linijka const user = req.user, nad każdym endpointem te same cztery dekoratory, a lista ról przepisywana ręcznie w dziesięciu miejscach. Jedna literówka i skarbiec stoi otworem. Rzym rozwiązał ten problem insygniami: pierścień senatora czy toga pretora od razu mówiły, kim jest ich właściciel i co mu wolno. W NestJS tę rolę pełnią custom decorators - własne pieczęcie, które przypinasz do metody, klasy albo parametru.
Pierścień senatora: dekorator parametru
Najczęściej tworzysz dekorator parametru. Wyciąga on dane z żądania i podaje je prosto do argumentu metody. Służy do tego funkcja createParamDecorator, która przyjmuje fabrykę z dwoma argumentami: data, czyli wartością wpisaną w nawias dekoratora, oraz ExecutionContext, znanym Ci już z lekcji o guardach:
1// decorators/current-user.decorator.ts
2import { createParamDecorator, ExecutionContext } from '@nestjs/common';
3
4// Dekorator @CurrentUser() - wyciąga dane użytkownika z requestu
5export const CurrentUser = createParamDecorator(
6 (data: string, ctx: ExecutionContext) => {
7 const request = ctx.switchToHttp().getRequest();
8 const user = request.user; // Ustawiony wcześniej przez AuthGuard
9
10 // Jeśli podano konkretne pole, zwróć tylko je
11 return data ? user?.[data] : user;
12 },
13);Fabryka uruchamia się przy każdym żądaniu, a jej wynik NestJS wstawia w miejsce parametru. Dekorator niczego nie pobiera z bazy i nie zmienia requestu - tylko odczytuje request.user, które wcześniej ustawił guard uwierzytelniający. Operator ?. chroni przed błędem, gdy użytkownika nie ma.
Tak wygląda użycie w kontrolerze legionu:
1// Użycie w kontrolerze
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' (tylko konkretne pole)
15 return { rank };
16 }
17}Zapis @CurrentUser('rank') oznacza, że data === 'rank', więc fabryka zwraca samo user.rank. Jedna uwaga praktyczna: ValidationPipe domyślnie nie waliduje parametrów z własnych dekoratorów, dopóki nie ustawisz opcji validateCustomDecorators: true.
Edykt cesarski: SetMetadata
Drugie narzędzie to metadane, czyli etykiety przyczepione do metody lub klasy. Funkcja SetMetadata(klucz, wartość) zapisuje taką etykietę, a my opakowujemy ją we własny dekorator, żeby nie powtarzać klucza w każdym kontrolerze:
1// decorators/roles.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4// Prosty dekorator @Roles() ustawiający metadane
5export const Roles = (...roles: string[]) => SetMetadata('roles', roles);Parametr resztowy ...roles zbiera wszystkie podane role w tablicę. Teraz przypinamy edykt do metod:
1// Użycie
2@Controller('tributa')
3export class TributeController {
4 @Post()
5 @Roles('consul', 'praetor') // Tylko consul i praetor mogą tworzyć
6 createTribute(@Body() dto: any) {
7 return { message: 'Trybut utworzony!' };
8 }
9
10 @Delete(':id')
11 @Roles('consul') // Tylko consul może usuwać
12 removeTribute(@Param('id') id: string) {
13 return { message: `Trybut ${id} usunięty` };
14 }
15}Uwaga, bo to najczęstsze nieporozumienie: same metadane niczego nie blokują. To edykt wywieszony na forum - obowiązuje dopiero wtedy, gdy ktoś go przeczyta i wyegzekwuje.
Pretorianin czyta edykt: Reflector w guardzie
Czytelnikiem jest guard. Reflector z pakietu @nestjs/core odczytuje metadane, a context.getHandler() wskazuje metodę, która za chwilę obsłuży żądanie:
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 // Odczytaj metadane 'roles' z handlera
11 const requiredRoles = this.reflector.get<string[]>(
12 'roles',
13 context.getHandler(),
14 );
15
16 // Brak wymaganych ról (albo pusta lista) = dostęp dla każdego zalogowanego
17 if (!requiredRoles?.length) return true;
18
19 const request = context.switchToHttp().getRequest();
20 const user = request.user;
21
22 // Sprawdź, czy użytkownik ma przynajmniej jedną wymaganą rolę
23 return requiredRoles.some(role => user?.roles?.includes(role));
24 }
25}Brak etykiety oznacza brak wymagań, więc guard przepuszcza żądanie. Warunek !requiredRoles?.length obejmuje też pustą listę - bez niego dekorator bez ról zablokowałby wszystkich, bo [].some() zwraca false. Pamiętaj też, że get() czyta metadane tylko z celu, który mu podasz - tu z metody (context.getHandler()). Jeśli @Roles() stawiasz również na klasie, użyj this.reflector.getAllAndOverride('roles', [context.getHandler(), context.getClass()]) - inaczej rola z klasy zostanie po cichu zignorowana.
Dokumentacja NestJS nazywa SetMetadata podejściem niskopoziomowym i pokazuje wariant z typowaniem:
1// decorators/roles.decorator.ts - wariant z Reflector.createDecorator
2import { Reflector } from '@nestjs/core';
3
4export const Roles = Reflector.createDecorator<string[]>();
5// użycie: @Roles(['consul', 'praetor'])
6// odczyt: this.reflector.get(Roles, context.getHandler())Tu kluczem jest sam dekorator, więc literówka w nazwie klucza przestaje być możliwa, a TypeScript pilnuje typu wartości. W nowym kodzie wybieram ten wariant. SetMetadata nadal działa i spotkasz go w wielu projektach.
Znacznik publiczny: @Public()
Nie każdy dekorator musi łączyć kilka innych. Ten ustawia jedną etykietę, a klucz eksportuje jako stałą, żeby guard mógł go odczytać bez przepisywania napisu:
1// decorators/public.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4// Dekorator oznaczający endpoint jako publiczny (bez autoryzacji)
5export const IS_PUBLIC_KEY = 'isPublic';
6export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
7
8// Użycie
9@Controller('forum')
10export class ForumController {
11 @Get()
12 @Public() // Ten endpoint jest publiczny
13 getPublicPosts() {
14 return { posts: ['Wiadomość z Forum Romanum'] };
15 }
16}Sam znacznik niczego nie otwiera. Działa w duecie z globalnym guardem uwierzytelniania, który najpierw sprawdza getAllAndOverride(IS_PUBLIC_KEY, ...) i przy wartości true przepuszcza żądanie bez tokenu.
Pieczęć główna: applyDecorators
Gdy nad endpointami powtarzasz ten sam zestaw dekoratorów, applyDecorators łączy je w jeden:
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// Złożony dekorator @Auth() łączący guard, role i dokumentację
8export function Auth(...roles: string[]) {
9 return applyDecorators(
10 SetMetadata('roles', roles),
11 UseGuards(JwtAuthGuard, RolesGuard),
12 ApiBearerAuth(),
13 ApiUnauthorizedResponse({ description: 'Brak autoryzacji' }),
14 );
15}W środku są cztery zwykłe dekoratory: metadane ról, dwa guardy oraz opisy dla Swaggera. applyDecorators nie dodaje nowej logiki - nakłada je po prostu wszystkie naraz.
1// Użycie - jeden dekorator zamiast czterech!
2@Controller('imperium')
3export class ImperiumController {
4 @Post('decree')
5 @Auth('consul', 'praetor') // Autoryzacja + role + Swagger
6 issueDecree(@Body() decree: any) {
7 return { message: 'Dekret wydany!', decree };
8 }
9
10 @Get('treasury')
11 @Auth('consul', 'quaestor') // Tylko consul i quaestor
12 getTreasury() {
13 return { treasury: 'Stan skarbca imperium' };
14 }
15}Jedna linijka zastępuje cztery, a guardy działają dokładnie tak samo jak przedtem. Zmiana zestawu w funkcji Auth obejmuje od razu cały projekt.
Praktyczne zastosowania
Na koniec dwa kolejne dekoratory metadanych, które razem z @Auth() i @CurrentUser() opisują rekrutację legionisty:
1// decorators/log-action.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4// Dekorator do logowania akcji w kronikach imperium
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// Użycie w kontrolerze
15@Controller('legiones')
16export class LegionController {
17 @Post('recruit')
18 @Auth('consul')
19 @LogAction('RECRUIT_LEGIONARY')
20 @RateLimit(10, 60000) // Max 10 rekrutacji na minutę
21 recruitLegionary(@Body() data: any, @CurrentUser() user: any) {
22 return {
23 message: `${user.name} zrekrutował nowego legionistę`,
24 legionary: data,
25 };
26 }
27}Pamiętaj o zasadzie z edyktu: @LogAction() zadziała dopiero z interceptorem, który odczyta klucz i zapisze wpis w kronice, a @RateLimit() - z guardem liczącym żądania. Do limitowania polecam gotowy pakiet @nestjs/throttler i dekorator @Throttle({ default: { limit: 10, ttl: 60000 } }), zamiast budować licznik od zera.
W następnej lekcji zbudujesz trybunały, czyli exception filters, a w module o uwierzytelnianiu @CurrentUser() odbierze użytkownika przygotowanego przez strategię JWT.
Pamiętaj: dekorator parametru wykonuje swoją fabrykę przy każdym żądaniu, a dekorator metadanych jest tylko pieczęcią, która działa dopiero wtedy, gdy guard albo interceptor ją odczyta.
Kod do tej lekcji: src/custom-decorators.ts
1// Własne dekoratory - Pieczęcie i Insygnia Imperium
2import { createParamDecorator, ExecutionContext, SetMetadata, applyDecorators, UseGuards } from '@nestjs/common';
3
4console.log("Własne dekoratory - tworzymy pieczęcie imperium!");
5
6// ===========================================
7// 1. Własny dekorator parametru
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() - wyciąga dane użytkownika z requestu");
19console.log("@CurrentUser('rank') - wyciąga konkretne pole");
20
21// ===========================================
22// 2. SetMetadata i Roles
23// ===========================================
24
25const Roles = (...roles: string[]) => SetMetadata('roles', roles);
26
27console.log("\n@Roles('consul', 'praetor') - ustawia wymagane role");
28console.log("Reflector.get('roles', handler) - odczytuje role w guardzie");
29
30// ===========================================
31// 3. applyDecorators - łączenie dekoratorów
32// ===========================================
33
34function Auth(...roles: string[]) {
35 return applyDecorators(
36 SetMetadata('roles', roles),
37 // UseGuards(JwtAuthGuard, RolesGuard),
38 );
39}
40
41console.log("\napplyDecorators() - łączy wiele dekoratorów w jeden");
42console.log("@Auth('consul') = SetMetadata('roles', ...) (w lekcji także UseGuards i ApiBearerAuth)");
43
44// ===========================================
45// 4. Praktyczny przykład
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=== PODSUMOWANIE WŁASNYCH DEKORATORÓW ===");
55console.log("createParamDecorator - dekorator parametru");
56console.log("SetMetadata - przypisanie metadanych");
57console.log("Reflector - odczyt metadanych w guardzie/interceptorze");
58console.log("applyDecorators - łączenie wielu dekoratorów");
59Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jakiej funkcji NestJS używamy do tworzenia własnego dekoratora parametru (np. @CurrentUser())?
2. Co reprezentuje argument 'data' w createParamDecorator((data, ctx) => ...)? Np. przy użyciu @CurrentUser('rank').
3. Do czego służy funkcja SetMetadata() w NestJS?
4. Co robi funkcja applyDecorators() w NestJS?
Zadania praktyczne w grze
- Edytor kodu
Napisz dekorator @CurrentUser() używając createParamDecorator, który zwraca request.user lub konkretne pole user[data]
- Klikanie w kolejności
Ułóż elementy tworzenia własnego dekoratora parametru w prawidłowej kolejności:
- Edytor kodu
Napisz dekorator @Roles() używając SetMetadata('roles', roles) oraz RolesGuard z Reflector, który sprawdza role użytkownika
- Układanie w poziomie
Ułóż elementy wywołania SetMetadata do ustawienia wymaganych ról:
- Układanie w pionie
Uporządkuj linie dekoratora @Auth() z lekcji od góry do dołu: