How to Avoid Forgetting to Secure Your NestJS Endpoints

January 6, 2026 Updated: January 6, 2026 Rens Jaspers NestJS TypeScript Webdev Security

When I build a public-facing REST API, I am always worried I will forget to secure one endpoint. At the same time, I want to avoid boilerplate and noisy controllers.

In this post I show a pattern that forces me to make an explicit choice for every endpoint, without making the code harder to read.

The problem with per-endpoint guards

Many examples show something like this:

@UseGuards(AuthGuard('jwt'), PermissionsGuard)
@Put()
@Permissions('update:items')
update(@Body('item') item: Item) {
  this.itemsService.update(item);
}

This works, but if you forget @UseGuards(...) on a new endpoint, that endpoint will accidentally become public.

It gets even more confusing if you do add @Permissions(...). It looks secure because you can see permissions on the method, but if you forgot the PermissionsGuard, those permissions are just metadata and nothing is enforced:

// Looks protected, but it is not
@Put()
@Permissions('update:items')
update(@Body('item') item: Item) {
  this.itemsService.update(item);
}

Also, repeating @UseGuards(...) everywhere makes your controllers cluttered.

Goal

  • Authentication is global.

  • Authorization is global.

  • Controllers stay clean: no @UseGuards(...) per endpoint.

  • Every endpoint is explicitly either:

    • @Public() (no auth needed), or
    • @Permissions(...) (permission needed)
  • If neither decorator exists, return 500 (error on our side).

1) Use two global guards

Use:

  • a global AuthenticationGuard
  • a global PermissionsGuard

Register them with APP_GUARD (instead of useGlobalGuards()) so you can keep using dependency injection.

// app.module.ts
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";

@Module({
  providers: [
    { provide: APP_GUARD, useClass: AuthenticationGuard },
    { provide: APP_GUARD, useClass: PermissionsGuard },
  ],
})
export class AppModule {}

2) Keep controllers simple

With global guards, you only declare permissions (or public access).

@Put()
@Permissions('update:items')
update(@Body('item') item: Item) {
  this.itemsService.update(item);
}

If an endpoint must be open:

@Get('health')
@Public()
health() {
  return { ok: true };
}

3) Decorators

// permissions.decorators.ts
import { SetMetadata } from "@nestjs/common";

export const IS_PUBLIC_KEY = "authorization:isPublic";
export const PERMISSIONS_KEY = "authorization:permissions";

export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

export const Permissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);

4) Fail closed: throw 500 if no decorator exists

This ensures safety: every endpoint must choose @Public() or @Permissions(...).

// permissions.guard.ts
import { CanActivate, ExecutionContext, Injectable, InternalServerErrorException } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { IS_PUBLIC_KEY, PERMISSIONS_KEY } from "./permissions.decorators";

@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(ctx: ExecutionContext): boolean {
    const handler = ctx.getHandler();
    const cls = ctx.getClass();

    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [handler, cls]);
    if (isPublic) return true;

    const permissions = this.reflector.getAllAndOverride<string[] | undefined>(PERMISSIONS_KEY, [handler, cls]);

    if (!permissions || permissions.length === 0) {
      throw new InternalServerErrorException(`Missing @Public() or @Permissions() on ${cls.name}.${handler.name} (error on our side)`);
    }

    // ... the rest of your authorization logic
    return true;
  }
}

With this setup, we do not have to attach guards to every endpoint, but we are still forced to choose: public, or protected with specific permissions.

This does not solve everything. You still need to think about your permission model, and you should not blindly copy/paste permissions. It is also a good idea to add tests that verify your endpoints are not accidentally public.

A working example: https://github.com/rensjaspers/nestjs-secure-by-default-demo