Introduction: Why Role-Based Authorization?

Welcome back! In the last lesson, you learned how to protect your API routes so that only authenticated users with a valid JWT can access them. That’s a great start, but in most real-world applications, not all users should have the same permissions. For example, you might want only administrators to be able to delete books or update user information, while regular users can only view data.

In the previous unit we only had JwtAuthGuard: any authenticated user could perform mutations (create/update/delete). That’s intentionally incomplete. In this unit we’ll require roles for sensitive actions. To make this concrete now, we’ll use an admin role (you already have a seeded admin user). In the SPA playground provided with the course you can try both a normal user and an admin; later, in the frontend path, you’ll build your own React UI.

This is where role-based authorization comes in. With role-based authorization, you can control what actions different types of users can perform in your API. In this lesson, you’ll learn how to implement this in your NestJS project so you can restrict certain endpoints to users with specific roles, like admin or user.

Quick Recap: Our API Structure

Before we dive in, let’s quickly remind ourselves of the current setup. You already have:

  • Registration (bcrypt hashes stored).
  • JWT Login (token with sub, role, iat, exp).
  • JwtAuthGuard (only checks if the token is valid → user is logged in).

Here’s a simplified code block to show where we are, focusing on how user roles fit in:

// Example: Protecting a route with JwtAuthGuard
@Patch(':id')
@UseGuards(JwtAuthGuard)
update(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() updateBookDto: UpdateBookDto,
) {
    const book = this.booksService.update(id, updateBookDto);
    return { success: true, data: book };
  }

What’s wrong

  • Any logged-in user can call write endpoints (e.g., create a book). That violates least-privilege.

What we’ll add

  • @Roles() decorator to declare required roles on a route.
  • RolesGuard to enforce those requirements after JwtAuthGuard authenticates the user.
  • BooksController write routes become admin-only (read routes remain public or authenticated as you decide).
Creating a Roles Decorator

In NestJS, a decorator is a special function you can use to add extra behavior to classes or methods. We’ll create a custom @Roles() decorator to mark which endpoints require certain roles.

Here’s how you can define it:

// src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export type Role = 'user' | 'admin';

export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

Explanation:

  • SetMetadata is a NestJS helper that attaches custom metadata to a route handler or controller.
  • ROLES_KEY is the key we’ll use to store the roles.
  • Role is a TypeScript type for allowed roles.
  • Roles(...roles) is the decorator you’ll use on your endpoints, like @Roles('admin').

What is metadata here?

In Nest, metadata is just key–value data attached to a class or handler at design time. Decorators like @Get() and @Post() also attach metadata. Nest provides the Reflector to read that metadata later (inside guards, interceptors, etc.).

Why SetMetadata?

SetMetadata(ROLES_KEY, roles) writes roles: [...] onto the route handler. The guard will read this value and decide access. This avoids hard-coding roles inside the guard; routes declare their own policy, the guard enforces it.

Example usage in the controller:

// Admin-only write; keep reads open or auth-only:
@Controller('books')
export class BooksController {
    constructor(private readonly booksService: BooksService) {}
    
    @Post()
    @UseGuards(AuthGuard('jwt'), RolesGuard)
    @Roles('admin')
    // Create a new book entry (admin only)
    create(@Body() createBookDto: CreateBookDto) {
        const book = this.booksService.create(createBookDto);
        return { success: true, data: book };
    }
    
    @Get()
    // Retrieve all books. No guards required here
    findAll() {
        const books = this.booksService.findAll();
        return { success: true, data: books };
    }
    
    //...rest of the methods
 }
Building and Using a Roles Guard

A guard in NestJS is a class that decides whether a request should be handled or rejected. We’ll create a RolesGuard that checks if the user’s role matches the required roles for an endpoint.

Here’s the code for the guard:

// src/common/guards/roles.guard.ts
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY, Role } from '../decorators/roles.decorator';

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

  canActivate(context: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!required || required.length === 0) return true;

    const req = context.switchToHttp().getRequest() as any;
    const role = req.user?.role as Role | undefined;

    if (!role || !required.includes(role)) {
      throw new ForbiddenException('Insufficient role');
    }
    return true;
  }
}

Explanation:

  • Reflector is used to read the metadata set by the @Roles() decorator.
  • canActivate is called before the route handler runs.
  • It checks if the endpoint has required roles. If not, it allows access.
  • It then checks the user’s role (from req.user.role).
  • If the user’s role is not in the required roles, it throws a ForbiddenException.
  • If the role matches, the request is allowed.

What happens if a user without the right role tries to access an endpoint?

They will get a response like:

{
  "statusCode": 403,
  "message": "Insufficient role",
  "error": "Forbidden"
}

Important: Order matters. use @UseGuards(JwtAuthGuard, RolesGuard) so authentication happens first, then authorization. Why getAllAndOverride? It checks metadata on the handler first, then on the controller, and returns the first non-undefined value. That lets you set a default policy at the controller level and override per route.

Applying Role-Based Authorization in Controllers

Now, let’s see how to use the @Roles() decorator and RolesGuard in your controllers. You’ll combine them with the JwtAuthGuard to make sure only authenticated users with the right role can access certain endpoints.

Here’s an example from the Books controller:

// src/books/books.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  ParseUUIDPipe,
  UseGuards,
} from '@nestjs/common';
import { BooksService } from './books.service';
import { CreateBookDto } from './dto/create-book.dto';
import { UpdateBookDto } from './dto/update-book.dto';
import { AuthGuard } from '@nestjs/passport';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';

@Controller('books')
export class BooksController {
  constructor(private readonly booksService: BooksService) {}

  @Post()
  @UseGuards(AuthGuard('jwt'), RolesGuard)
  @Roles('admin')
  // Create a new book entry (admin only)
  create(@Body() createBookDto: CreateBookDto) {
    const book = this.booksService.create(createBookDto);
    return { success: true, data: book };
  }

  @Get()
  // Retrieve all books
  findAll() {
    const books = this.booksService.findAll();
    return { success: true, data: books };
  }

  @Get(':id')
  // Retrieve a specific book by ID
  findOne(@Param('id', ParseUUIDPipe) id: string) {
    const book = this.booksService.findOne(id);
    return { success: true, data: book };
  }

  @Patch(':id')
  @UseGuards(AuthGuard('jwt'), RolesGuard)
  @Roles('admin')
  // Update a book's data (admin only)
  update(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() updateBookDto: UpdateBookDto,
  ) {
    const book = this.booksService.update(id, updateBookDto);
    return { success: true, data: book };
  }

  @Delete(':id')
  @UseGuards(AuthGuard('jwt'), RolesGuard)
  @Roles('admin')
  // Remove a book by ID (admin only)
  remove(@Param('id', ParseUUIDPipe) id: string) {
    const removed = this.booksService.remove(id);
    return { success: true, data: removed };
  }
}

Explanation:

  • @UseGuards(JwtAuthGuard, RolesGuard) applies both guards to the endpoint.
  • @Roles('admin') marks this endpoint as admin-only.
  • Only authenticated users with the admin role can create books.

Here’s another example from the Users controller:

// src/users/users.controller.ts

//...other endpoints 

@Patch(':id')
// Update an existing user
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles('admin')
update(@Param('id', ParseUUIDPipe) id: string, @Body() updateUserDto: UpdateUserDto) {
    const user = this.usersService.update(id, updateUserDto);
    return { success: true, data: user };
}

Explanation:

  • Only admins can update user information.
  • If a regular user tries to access this endpoint, they will get a 403 Forbidden error.
Try it in terminal (and in the SPA playground)

Admin creates a book (expect 201):

curl -sS -X POST "http://localhost:3000/books" \
  -H "Authorization: Bearer <ADMIN_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"title":"Clean Architecture","author":"Robert C. Martin"}'

Regular user tries the same (expect 403):

curl -sS -X POST "http://localhost:3000/books" \
  -H "Authorization: Bearer <USER_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"title":"User Book","author":"Some User"}'

Public/less restricted read:

curl -sS "http://localhost:3000/books"
Summary And Practice Preview

In this lesson, you learned how to add role-based authorization to your NestJS API. You created a custom @Roles() decorator to mark which endpoints require certain roles and a RolesGuard to enforce these rules. You also saw how to apply these tools in your controllers to restrict actions like creating or deleting books to admins only.

Key outcomes to verify in your environment:

  • Write endpoints (POST/PATCH/DELETE /books) return 403 for a regular user and 2xx for an admin.
  • Read endpoints remain reachable as designed (public or auth-only).
  • Swapping guard order to @UseGuards(RolesGuard, JwtAuthGuard) fails correctly because req.user won’t exist—this confirms you understand the dependency between guards.

Role-based authorization is a key part of building secure and flexible APIs. It lets you control exactly who can do what in your application.

Next, you’ll get a chance to practice these concepts yourself. You’ll write and test endpoints with role-based restrictions, so you can be confident in securing your own APIs. Great work getting this far — let’s keep going!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal