Month 4, Week 3

Authentication & Advanced NestJS Patterns

Guards, Pipes, and Interceptors

Module 1: Authentication with Guards

Architecting the Gatekeeper

Recap: Middleware in Express

In Express, we wrote a middleware function to protect routes. It worked, but it was not tightly integrated with our application's architecture.


                        function authMiddleware(req, res, next) {
                          // Manually check header
                          // Manually verify token
                          // Manually attach user to `req`
                          // Manually handle errors
                          // Call `next()`
                        }

                        app.get('/profile', authMiddleware, getProfile);
                    

The NestJS Way: Guards

A Guard is a class with a single responsibility: it determines whether a given request will be handled by the route handler or not. It makes a "yes" or "no" decision.

They are the perfect tool for authentication and authorization.

A Guard implements the `CanActivate` interface, which requires a single method: `canActivate()`.

Building an `AuthGuard`

This guard will implement our JWT verification logic.


                        import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
                        import { JwtService } from '@nestjs/jwt'; // A NestJS provided service

                        @Injectable()
                        export class AuthGuard implements CanActivate {
                          constructor(private jwtService: JwtService) {}

                          async canActivate(context: ExecutionContext): Promise {
                            const request = context.switchToHttp().getRequest();
                            const token = this.extractTokenFromHeader(request);
                            if (!token) {
                              throw new UnauthorizedException();
                            }
                            try {
                              const payload = await this.jwtService.verifyAsync(token, { secret: 'YOUR_SECRET' });
                              // We're assigning the payload to the request object here
                              // so that we can access it in our route handlers
                              request['user'] = payload;
                            } catch {
                              throw new UnauthorizedException();
                            }
                            return true; // If no error was thrown, access is granted
                          }

                          private extractTokenFromHeader(request: Request): string | undefined {
                            const [type, token] = request.headers.authorization?.split(' ') ?? [];
                            return type === 'Bearer' ? token : undefined;
                          }
                        }
                    

Using the `AuthGuard`

Guards are applied using the `@UseGuards()` decorator. This is far cleaner than passing middleware functions manually.


                        import { UseGuards, Get, Req } from '@nestjs/common';
                        import { AuthGuard } from '../auth/auth.guard';

                        @Controller('profile')
                        export class ProfileController {
                          
                          @UseGuards(AuthGuard) // Apply the guard to this route
                          @Get()
                          getProfile(@Req() req) {
                            // Because the guard ran, we know `req.user` exists
                            return req.user;
                          }
                        }
                    

You can apply `@UseGuards` at the method level (for a single route) or at the class level (for all routes in a controller).

Module 2: Pipes Deep Dive

Transformation and Validation

What is a Pipe?

A Pipe is a class that operates on the arguments being passed to a route handler. It has two primary use cases:

  1. Transformation: Transform input data from one form to another (e.g., a string `id` from the URL into a number).
  2. Validation: Evaluate input data and, if it's invalid, throw an exception.

Built-in Pipes

NestJS comes with a powerful set of built-in pipes that handle common tasks.

  • `ValidationPipe`: We saw this last week. It automatically validates DTOs.
  • `ParseIntPipe`: Parses a string into an integer. Throws an exception if the string is not a valid number.
  • `ParseUUIDPipe`: Validates that a string is a valid UUID.
  • `DefaultValuePipe`: Provides a default value for a parameter if it's `undefined`.

Using `ParseIntPipe`

This pipe automatically handles the validation and transformation of route parameters.

Before (Manual):


                        @Get(':id')
                        findOne(@Param('id') id: string) {
                            const numericId = parseInt(id, 10);
                            if (isNaN(numericId)) {
                                throw new BadRequestException('ID must be a number.');
                            }
                            return this.usersService.findOne(numericId);
                        }
                    

After (Using the Pipe):


                        @Get(':id')
                        findOne(@Param('id', ParseIntPipe) id: number) {
                            // `id` is now guaranteed to be a number.
                            // The pipe handles the error if it's not.
                            return this.usersService.findOne(id);
                        }
                    

Mid-Lecture Knowledge Check

Module 3: Interceptors

Binding Cross-Cutting Concerns

What is an Interceptor?

An Interceptor is a class that allows you to run logic before and after a route handler executes. It can:

  • Bind extra logic before/after method execution.
  • Transform the result returned from a function.
  • Transform the exception thrown from a function.
  • Extend the basic function behavior.

They are perfect for cross-cutting concerns like logging, caching, or standardizing your API response format.

Building a Logging Interceptor

This interceptor will log the time it takes for a request to be processed.


                        @Injectable()
                        export class LoggingInterceptor implements NestInterceptor {
                          intercept(context: ExecutionContext, next: CallHandler): Observable {
                            console.log('Before...'); // Logic before the handler runs

                            const now = Date.now();
                            return next
                              .handle() // This calls the route handler
                              .pipe(
                                tap(() => console.log(`After... ${Date.now() - now}ms`)), // Logic after the handler runs
                              );
                          }
                        }
                    

Interceptors use RxJS `Observables`, which is a powerful library for handling asynchronous streams.

The Most Powerful Use Case: Response Transformation

A professional API should always have a consistent response structure. We can enforce this with an interceptor.

**Goal:** Transform all successful responses from `` into `{ "statusCode": 200, "data": }`.


                        import { map } from 'rxjs/operators';

                        @Injectable()
                        export class TransformInterceptor implements NestInterceptor {
                          intercept(context: ExecutionContext, next: CallHandler): Observable<{ statusCode: number, data: T }> {
                            const statusCode = context.switchToHttp().getResponse().statusCode;
                            return next.handle().pipe(
                                map(data => ({ statusCode, data }))
                            );
                          }
                        }
                     

Using the Interceptor

Like Guards, Interceptors are applied with a decorator: `@UseInterceptors()`.

If you apply this globally in `main.ts`, every single response from your API will be automatically wrapped in the standard format!


                        // main.ts
                        app.useGlobalInterceptors(new TransformInterceptor());

                        // In your controller
                        @Get()
                        findAll() {
                          return this.usersService.findAll(); // This just returns an array of users
                        }

                        // The client receives:
                        // {
                        //   "statusCode": 200,
                        //   "data": [ { "id": 1, "name": "Alex" }, ... ]
                        // }
                    

In-Class Practical Exercise

Implementing a Role-Based Guard

You will be given a NestJS project with a working JWT `AuthGuard`. Your task is to create a new `RolesGuard` to implement simple authorization.

  1. Create a `RolesGuard` that checks for roles on the `req.user` object (which is attached by the `AuthGuard`).
  2. The roles required for a route will be passed as metadata. You will need to use the `@SetMetadata('roles', ['admin'])` decorator on your route.
  3. Inside the `RolesGuard`, use the `Reflector` service to read this metadata.
  4. Compare the required roles from the metadata with the user's roles from the JWT payload (`req.user.roles`).
  5. If the user has the required role, return `true`. Otherwise, throw a `ForbiddenException`.
  6. Apply your new `RolesGuard` to a "delete user" endpoint, requiring an 'admin' role. Remember to apply the `AuthGuard` first!
    @UseGuards(AuthGuard, RolesGuard)
    @SetMetadata('roles', ['admin'])
    @Delete(':id')
    remove(...) { /* ... */ }
  7. Test your endpoint with JWTs for both an admin and a regular user.

Final Knowledge Check