Bulletproof Your API: A Deep Dive into Custom Guards and Decorators in NestJS

In any robust backend application, security and clean code are paramount. Learn how NestJS's powerful combination of Guards and Custom Decorators can help you build secure, maintainable APIs.


Bulletproof Your API: A Deep Dive into Custom Guards and Decorators in NestJS

In any robust backend application, security and clean code are paramount. You need to protect certain endpoints, ensuring that only authorized clients can access them. At the same time, you want your controller logic to remain clean, focused on business tasks, not cluttered with boilerplate authentication and request-parsing code.

This is where NestJS’s powerful combination of Guards and Custom Decorators shines. A Guard acts as a gatekeeper for your routes, while a Custom Decorator can cleanly extract information from the request and provide it to your handler.

Let’s walk through a common real-world scenario: protecting a webhook endpoint with an API key. We’ll build a custom ApiKeyGuard to protect the route and a slick @ApiKey() decorator to inject the key directly into our controller method.

The Problem: Cluttered and Repetitive Security Logic

Imagine you have a controller with a webhook that receives data from an external service. You need to ensure this request includes a valid API key in its headers. Without a guard, you might end up writing this logic directly in your controller:

// The "before" - cluttered controller logic
@Controller("webhooks")
export class WebhookController {
  @Post("/my-webhook")
  handleWebhook(@Req() request: Request, @Body() data: any) {
    const apiKey = request.headers["x-api-key"];

    if (!apiKey || apiKey !== process.env.SECRET_API_KEY) {
      throw new UnauthorizedException("Invalid or missing API key.");
    }

    // --- Finally, the actual business logic ---
    this.webhookService.process(data);
    return { status: "ok" };
  }
}

This works, but it has problems. The security check is mixed in with the business logic, and if you have multiple webhook endpoints, you’ll have to repeat this code everywhere, which is a clear violation of the DRY (Don’t Repeat Yourself) principle.

Step 1: The Gatekeeper - A Custom Guard

A NestJS Guard is a class that implements the CanActivate interface. It has one job: to decide if a given request should be allowed to proceed. Let’s create an ApiKeyGuard.

// In src/guards/api-key.guard.ts

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from "@nestjs/common";

@Injectable()
export class ApiKeyGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const apiKey = request.headers["x-api-key"];

    if (!apiKey) {
      throw new UnauthorizedException("API key is missing.");
    }

    // Compare the incoming key with the one stored in your environment variables
    if (apiKey !== process.env.SECRET_API_KEY) {
      throw new UnauthorizedException("Invalid API key.");
    }

    // If we reach here, the key is valid. Allow the request.
    return true;
  }
}

This class is simple, focused, and reusable. It does one thing, and it does it well. Now, we can apply this guard to our controller using the @UseGuards decorator.

// The "after" - a much cleaner controller
import { ApiKeyGuard } from "../guards/api-key.guard";

@Controller("webhooks")
export class WebhookController {
  @Post("my-webhook")
  @UseGuards(ApiKeyGuard) // Apply the guard here!
  handleWebhook(@Body() data: any) {
    // No more security logic! Just business logic.
    this.webhookService.process(data);
    return { status: "ok" };
  }
}

This is a massive improvement! Our controller is now clean, and our security logic is encapsulated in a single, testable, and reusable class.

Step 2: The Data Extractor - A Custom Decorator

Our guard protects the route, but what if the handler method actually needs to know which API key was used? (Perhaps for logging or to look up which external partner sent the request).

We could access the request object again with @Req(), but that’s still messy. A much cleaner way is to create a custom parameter decorator to extract the API key for us.

Let’s create an @ApiKey() decorator.

// In src/decorators/api-key.decorator.ts

import { createParamDecorator, ExecutionContext } from "@nestjs/common";

export const ApiKey = createParamDecorator(
  (data: unknown, ctx: ExecutionContext): string | null => {
    const request = ctx.switchToHttp().getRequest();
    return request.headers["x-api-key"] || null;
  },
);

This decorator is a simple function that extracts the x-api-key from the request headers. Now we can use it directly in our controller’s method signature.

The Final Result: A Symphony of Guards and Decorators

By combining our guard and our new decorator, we arrive at a final implementation that is secure, clean, and incredibly declarative.

// The final, elegant implementation
import { ApiKeyGuard } from "../guards/api-key.guard";
import { ApiKey } from "../decorators/api-key.decorator";

@Controller("webhooks")
export class WebhookController {
  @Post("my-webhook")
  @UseGuards(ApiKeyGuard)
  handleWebhook(
    @Body() data: any,
    @ApiKey() usedKey: string, // The decorator injects the key directly!
  ) {
    this.webhookService.logAccess(usedKey);
    this.webhookService.process(data);
    return { status: "ok" };
  }
}

This is the pinnacle of what NestJS enables. Our controller is now very easy to read. You can see at a glance:

  • It’s a POST endpoint for /webhooks/my-webhook.
  • It’s protected by our ApiKeyGuard.
  • It expects a request body (data).
  • It gets the usedKey from our custom @ApiKey() decorator.

The business logic is completely decoupled from the security and request-parsing logic. This pattern of creating focused Guards for protection and focused Decorators for data extraction is a cornerstone of building robust, maintainable, and “bulletproof” APIs with NestJS.