0Pricing
NestJS Enterprise Backend APIs · 강의

applyDecorators로 데코레이터 조합하기

Swagger, 검증 및 인증 데코레이터를 하나의 편리한 @ApiSecureEndpoint 데코레이터로 묶습니다

applyDecorators로 데코레이터 조합하기은(는) CoddyKit의 무료 NestJS Enterprise Backend APIs 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 NestJS Enterprise Backend APIs 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

The Decorator Stacking Problem

In a real NestJS enterprise API, almost every route handler ends up carrying a tall stack of decorators: Swagger docs, auth guards, role checks, and response shaping. Repeating that stack on every endpoint is verbose and error-prone.

  • Duplication: the same six lines copied onto dozens of handlers.
  • Drift: someone forgets @ApiBearerAuth() on one route and the docs lie.
  • Noise: the actual business intent is buried under cross-cutting concerns.

This lesson teaches how to collapse a recurring decorator stack into a single reusable @ApiSecureEndpoint() using NestJS's applyDecorators.

// The pain: this stack repeats on every secured route
@Post('transfer')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@ApiBearerAuth()
@ApiOperation({ summary: 'Move funds between accounts' })
@ApiOkResponse({ description: 'Transfer accepted' })
@ApiUnauthorizedResponse({ description: 'Missing or invalid token' })
async transfer(@Body() dto: TransferDto) {
  return this.bank.transfer(dto);
}

What applyDecorators Actually Does

applyDecorators is a helper from @nestjs/common. It takes any number of decorators and returns one new decorator that applies all of them in order when used.

  • It works with method decorators, class decorators, and property decorators.
  • The decorators run top-to-bottom, exactly as if you had written them by hand.
  • It does not change behavior — it only composes. Whatever the original stack did, the composed decorator does identically.

Think of it as function composition for the decorator world.

import { applyDecorators, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger';

export function SecureRoute() {
  return applyDecorators(
    UseGuards(JwtAuthGuard),
    ApiBearerAuth(),
    ApiOperation({ summary: 'Protected route' }),
  );
}

A Custom Decorator Is Just a Function

Before composing, recall the shape of a custom decorator. A method decorator is a function that receives the target, the property key, and the property descriptor. NestJS decorators like UseGuards() are decorator factories: calling them returns such a function.

This plain-TypeScript example shows the mechanics with no framework involved — a logging decorator factory applied to a class method.

function LogCalls(label: string) {
  return function (
    _target: object,
    key: string,
    descriptor: PropertyDescriptor,
  ) {
    const original = descriptor.value;
    descriptor.value = function (...args: unknown[]) {
      console.log(`[${label}] ${key} called`);
      return original.apply(this, args);
    };
  };
}

class Calculator {
  @LogCalls('math')
  add(a: number, b: number): number {
    return a + b;
  }
}

const c = new Calculator();
console.log('result =', c.add(2, 3));

Composing Without a Helper

To appreciate applyDecorators, see what manual composition looks like in pure TypeScript. A composer returns a single method decorator that loops over the inner decorators and invokes each.

This standalone snippet mirrors how NestJS's applyDecorators works under the hood — running every decorator against the same target.

type MethodDec = (t: object, k: string, d: PropertyDescriptor) => void;

function compose(...decorators: MethodDec[]): MethodDec {
  return (target, key, descriptor) => {
    for (const dec of decorators) {
      dec(target, key, descriptor);
    }
  };
}

const tag = (name: string): MethodDec => (_t, k) =>
  console.log(`applied ${name} to ${k}`);

class Service {
  @compose(tag('auth'), tag('swagger'), tag('roles'))
  handle(): string {
    return 'ok';
  }
}

console.log(new Service().handle());

Building the First Version of @ApiSecureEndpoint

Now bundle the realistic stack. Our goal decorator @ApiSecureEndpoint() should attach JWT auth, role enforcement, the Swagger bearer scheme, and the common error responses.

  • UseGuards wires the runtime protection.
  • ApiBearerAuth tells Swagger UI to send the token.
  • The ApiUnauthorizedResponse / ApiForbiddenResponse document the failure modes once and for all.
import { applyDecorators, UseGuards } from '@nestjs/common';
import {
  ApiBearerAuth,
  ApiUnauthorizedResponse,
  ApiForbiddenResponse,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../auth/roles.guard';

export function ApiSecureEndpoint() {
  return applyDecorators(
    UseGuards(JwtAuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Missing or invalid token' }),
    ApiForbiddenResponse({ description: 'Insufficient permissions' }),
  );
}

Using the Composed Decorator

With the composer in place, the controller collapses to a single intent-revealing line per route. The four concerns are still active — they are just declared once inside the factory.

Compare this to scene 1: the same protection and documentation, far less noise.

@Controller('accounts')
export class AccountsController {
  constructor(private readonly bank: BankService) {}

  @Post('transfer')
  @ApiSecureEndpoint()
  @ApiOperation({ summary: 'Move funds between accounts' })
  async transfer(@Body() dto: TransferDto) {
    return this.bank.transfer(dto);
  }
}

Passing Arguments Into the Composer

The real ergonomic win comes from parameterizing the factory. Because ApiSecureEndpoint is a function, it can accept options and forward them to the inner decorators — for example the required roles and a summary string.

This lets one decorator express the full security + docs contract of a route in a single, readable call.

import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiForbiddenResponse } from '@nestjs/swagger';

export const ROLES_KEY = 'roles';

export function ApiSecureEndpoint(opts: { summary: string; roles?: string[] }) {
  return applyDecorators(
    UseGuards(JwtAuthGuard, RolesGuard),
    SetMetadata(ROLES_KEY, opts.roles ?? []),
    ApiBearerAuth(),
    ApiOperation({ summary: opts.summary }),
    ApiForbiddenResponse({ description: 'Insufficient permissions' }),
  );
}

Folding in Validation and Response Typing

Enterprise endpoints also document their success payload. applyDecorators can fold in a typed ApiOkResponse, and you can combine it with a serialization interceptor so the contract is enforced both in code and in the docs.

  • type drives the Swagger response schema and example.
  • Adding UseInterceptors(ClassSerializerInterceptor) guarantees the DTO transforms apply.
import { applyDecorators, UseGuards, UseInterceptors, ClassSerializerInterceptor, Type } from '@nestjs/common';
import { ApiBearerAuth, ApiOkResponse, ApiOperation } from '@nestjs/swagger';

export function ApiSecureEndpoint(opts: {
  summary: string;
  type: Type<unknown>;
}) {
  return applyDecorators(
    UseGuards(JwtAuthGuard, RolesGuard),
    UseInterceptors(ClassSerializerInterceptor),
    ApiBearerAuth(),
    ApiOperation({ summary: opts.summary }),
    ApiOkResponse({ type: opts.type, description: 'Success' }),
  );
}

Order Matters for Guards and Interceptors

Within a composed decorator, the relative order of UseGuards and UseInterceptors matters at runtime. NestJS runs guards before interceptors regardless, but when you list multiple guards their execution order follows the array, and multiple interceptors nest in declaration order.

  • Put authentication guards before authorization guards so an unauthenticated request short-circuits with 401, not 403.
  • Swagger-only decorators (ApiOperation, ApiOkResponse) have no runtime ordering effect — they only attach metadata.
// JwtAuthGuard first => 401 before RolesGuard ever runs => correct
UseGuards(JwtAuthGuard, RolesGuard)

// Reversed => RolesGuard may read an empty user and throw 403
// for a request that was actually just unauthenticated
UseGuards(RolesGuard, JwtAuthGuard) // avoid

Keeping the Decorator Testable

Because a composed decorator is a plain factory function, you can unit-test that it wires the metadata you expect without booting Nest. Use the Reflector or read metadata keys directly off the decorated method.

This standalone TypeScript example demonstrates the underlying idea — reading back metadata that a decorator attached via Reflect.defineMetadata.

import 'reflect-metadata';

function Roles(...roles: string[]) {
  return (t: object, k: string) =>
    Reflect.defineMetadata('roles', roles, t, k);
}

class Ctrl {
  @Roles('admin', 'auditor')
  remove() {}
}

const meta = Reflect.getMetadata('roles', Ctrl.prototype, 'remove');
console.log('declared roles =', meta);
console.log('admin allowed =', meta.includes('admin'));

When NOT to Compose

Composition is powerful but can hide important details. Reach for a composed decorator only when the stack is genuinely repeated and stable.

  • Do compose a fixed security + docs envelope used across many routes.
  • Avoid composing highly route-specific details like a unique ApiOperation summary or one-off query params — pass those as arguments or leave them inline.
  • Avoid burying rarely-used behavior; a reader should still grasp what a route does from its decorators.

Good composed decorators reduce noise without becoming a black box.

Quick Check: Composing Decorators

You want a single @ApiSecureEndpoint() that applies a JWT guard, a roles guard, the Swagger bearer scheme, and shared error responses. Which approach is idiomatic in NestJS?

Recap & Takeaways

You learned to collapse a repetitive decorator stack into one ergonomic decorator.

  • applyDecorators from @nestjs/common composes any number of decorators into a single one that applies them in order.
  • A composed decorator is just a factory function, so it can accept options (roles, summary, response type) and forward them to the inner decorators.
  • Bundle stable cross-cutting concerns — UseGuards, ApiBearerAuth, shared ApiUnauthorizedResponse/ApiForbiddenResponse — and keep route-specific details as arguments or inline.
  • Mind guard ordering: authentication before authorization so a missing token yields 401, not 403.
  • Because it is plain TypeScript, the composed decorator stays testable via reflected metadata.

Result: controllers that read as intent (@ApiSecureEndpoint({ summary, roles })) instead of a wall of boilerplate.

자주 묻는 질문

“applyDecorators로 데코레이터 조합하기” 강의는 무료인가요?

네 — “applyDecorators로 데코레이터 조합하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 NestJS Enterprise Backend APIs 강의 전체를 잠금 해제할 수 있습니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.

“applyDecorators로 데코레이터 조합하기”에서 뭘 배우나요?

Swagger, 검증 및 인증 데코레이터를 하나의 편리한 @ApiSecureEndpoint 데코레이터로 묶습니다 브라우저에서 직접 실행하는 실습 코드로 NestJS Enterprise Backend APIs을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

NestJS Enterprise Backend APIs을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 NestJS Enterprise Backend APIs은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“applyDecorators로 데코레이터 조합하기” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 NestJS Enterprise Backend APIs 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 NestJS Enterprise Backend APIs 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 매개변수 데코레이터로 요청 컨텍스트 읽기
  2. SetMetadata와 Reflector로 메타데이터 연결하기
  3. applyDecorators로 데코레이터 조합하기
  4. 공통 설정을 위한 클래스 수준 데코레이터
← NestJS Enterprise Backend APIs(으)로 돌아가기