Kurs NestJS · Moduł 2: Routing i cykl żądania

Własne dekoratory - pieczęcie i insygnia imperium

7 min czytania
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");
59

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jakiej funkcji NestJS używamy do tworzenia własnego dekoratora parametru (np. @CurrentUser())?

  2. 2. Co reprezentuje argument 'data' w createParamDecorator((data, ctx) => ...)? Np. przy użyciu @CurrentUser('rank').

  3. 3. Do czego służy funkcja SetMetadata() w NestJS?

  4. 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:

Przydatne artykuły