Menu
CoddyTech

NestJS Middleware, Guards and Interceptors Questions

What runs before and after a route handler, in which order, and when to use each piece.

11 questions
  1. In what order do middleware, guards, interceptors, pipes and filters run in NestJS?

    Middleware, then guards, then interceptors (before the handler), then pipes, then the route handler, then interceptors again on the way out. Exception filters run only when something throws, at any of those steps.

    NestJS
    import { CallHandler, CanActivate, Controller, ExecutionContext, Get, MiddlewareConsumer,
      Module, NestInterceptor, NestMiddleware, Param, PipeTransform } from '@nestjs/common';
    import { NestFactory } from '@nestjs/core';
    import { tap } from 'rxjs';
    const steps: string[] = [];
    
    class LogMiddleware implements NestMiddleware {
      use(req: any, res: any, next: () => void) { steps.length = 0; steps.push('middleware'); next(); }
    }
    class AuthGuard implements CanActivate {
      canActivate(ctx: ExecutionContext) { steps.push('guard'); return true; }
    }
    class TimingInterceptor implements NestInterceptor {
      intercept(ctx: ExecutionContext, next: CallHandler) {
        steps.push('interceptor (before)');
        return next.handle().pipe(tap(() => steps.push('interceptor (after)')));
      }
    }
    class IdPipe implements PipeTransform {
      transform(value: string) { steps.push('pipe'); return Number(value); }
    }
    
    // GET /orders/7
    @Controller('orders')
    class OrdersController {
      @Get(':id')
      find(@Param('id', IdPipe) id: number) {
        steps.push('handler');
        return { id, steps };
      }
    }
    
    @Module({ controllers: [OrdersController] })
    class AppModule {
      configure(consumer: MiddlewareConsumer) { consumer.apply(LogMiddleware).forRoutes(OrdersController); }
    }
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      app.useGlobalGuards(new AuthGuard());
      app.useGlobalInterceptors(new TimingInterceptor());
      await app.listen(3000);
    }
    bootstrap();

    The response lists the steps for GET /orders/7. interceptor (after) is in it because Nest serializes the response only after the interceptor's stream completes. Make the guard return false and send the request again: you get a 403 and the list never reaches pipe.

  2. How do you write and apply middleware in NestJS?

    A middleware is a class implementing NestMiddleware (or a plain function) with the Express signature (req, res, next). Apply it in a module's configure() method:

    @Injectable()
    export class RequestIdMiddleware implements NestMiddleware {
      use(req: Request, res: Response, next: NextFunction) {
        res.setHeader('x-request-id', crypto.randomUUID());
        next();
      }
    }
    
    @Module({ controllers: [OrdersController] })
    export class OrdersModule implements NestModule {
      configure(consumer: MiddlewareConsumer) {
        consumer
          .apply(RequestIdMiddleware)
          .exclude({ path: 'orders/health', method: RequestMethod.GET })
          .forRoutes(OrdersController);
      }
    }
    

    Forget next() and the request hangs. For app-wide plain middleware, app.use() in main.ts also works. See Express middleware.

  3. What is the difference between middleware and a guard?

    Middleware runs before Nest has picked a handler, so it only knows the raw request. A guard runs after routing and receives an ExecutionContext, so it knows which controller and method will run and can read their metadata (@Roles('admin')).

    MiddlewareGuard
    Knows the handlerNoYes
    ReturnsCalls next()true or false (or throws)
    Typical useLogging, request ids, body parsingAuthentication, authorization
    Works for microservices, WebSocketsNo (HTTP only)Yes

    If the decision depends on the route, write a guard.

  4. What is a guard in NestJS? Show a role check.

    A guard is a class implementing CanActivate. It decides whether the request reaches the handler: returning false makes Nest answer 403 Forbidden, and throwing an exception sends that exception instead.

    NestJS
    import { Controller, Get, Module, Injectable, CanActivate, ExecutionContext, UseGuards } from '@nestjs/common';
    import { NestFactory, Reflector } from '@nestjs/core';
    
    export const Roles = Reflector.createDecorator<string[]>();
    
    @Injectable()
    class RolesGuard implements CanActivate {
      constructor(private reflector: Reflector) {}
      canActivate(ctx: ExecutionContext): boolean {
        const needed = this.reflector.get(Roles, ctx.getHandler());
        if (!needed) return true;
        const role = ctx.switchToHttp().getRequest().headers['x-role'];
        return needed.includes(role);
      }
    }
    
    // GET /reports
    @Controller('reports')
    @UseGuards(RolesGuard)
    class ReportsController {
      @Get()
      @Roles(['admin'])
      all() { return ['q1', 'q2']; }
    
      @Get('public')
      open() { return 'anyone can read this'; }
    }
    
    @Module({ controllers: [ReportsController], providers: [RolesGuard] })
    class AppModule {}
    
    async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); }
    bootstrap();

    The first request has no role, so it answers 403 with Forbidden resource. Add the header x-role: admin in the request bar and send it again: you get ["q1","q2"]. GET /reports/public has no @Roles, so the guard lets it through. Reflector.createDecorator() builds a typed metadata decorator; older code does the same with SetMetadata('roles', roles).

  5. How do you protect routes with JWT authentication in NestJS?

    Write a guard that reads the Authorization: Bearer <token> header, verifies the token with JwtService from @nestjs/jwt, and attaches the payload to the request. Throw UnauthorizedException (401) when it is missing or invalid.

    @Injectable()
    export class AuthGuard implements CanActivate {
      constructor(private jwt: JwtService) {}
    
      async canActivate(ctx: ExecutionContext) {
        const req = ctx.switchToHttp().getRequest();
        const [type, token] = req.headers.authorization?.split(' ') ?? [];
        if (type !== 'Bearer' || !token) throw new UnauthorizedException();
        try {
          req.user = await this.jwt.verifyAsync(token);
        } catch {
          throw new UnauthorizedException();
        }
        return true;
      }
    }
    

    Register it globally with APP_GUARD and mark open routes with a @Public() metadata decorator the guard checks. Passport (@nestjs/passport) is the other common route. JWT in Node.js shows the token itself.

  6. What is an interceptor in NestJS, and what is it used for?

    An interceptor wraps the handler call. Its intercept() gets the context and a CallHandler; next.handle() returns an RxJS Observable of the handler's result, so it can run code before the handler and transform, time, cache or replace the result after it.

    NestJS
    import {
      CallHandler, Controller, createParamDecorator, ExecutionContext, Get,
      Injectable, Module, NestInterceptor, UseInterceptors,
    } from '@nestjs/common';
    import { NestFactory } from '@nestjs/core';
    import { map } from 'rxjs';
    
    @Injectable()
    class EnvelopeInterceptor implements NestInterceptor {
      intercept(ctx: ExecutionContext, next: CallHandler) {
        const req = ctx.switchToHttp().getRequest();
        return next.handle().pipe(map((data) => ({ data, path: req.url, version: 1 })));
      }
    }
    
    const Lang = createParamDecorator((fallback: string, ctx: ExecutionContext) => {
      const req = ctx.switchToHttp().getRequest();
      return req.headers['accept-language']?.slice(0, 2) || fallback;
    });
    
    // GET /greet
    @Controller('greet')
    @UseInterceptors(EnvelopeInterceptor)
    class GreetController {
      @Get()
      hello(@Lang('en') lang: string) {
        return lang === 'fr' ? 'Bonjour' : 'Hello';
      }
    }
    
    @Module({ controllers: [GreetController] })
    class AppModule {}
    
    async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); }
    bootstrap();

    The handler returns the string Hello, but the response is wrapped in { data, path, version } by the interceptor. Send it again with the header Accept-Language: fr-FR to get Bonjour (read by the custom @Lang() decorator). Common uses: response envelopes, logging with timing, timeout(), and caching.

  7. How do you create a custom decorator like @CurrentUser()?

    For a parameter, use createParamDecorator, which receives the decorator's argument and the ExecutionContext:

    export const CurrentUser = createParamDecorator(
      (field: keyof User | undefined, ctx: ExecutionContext) => {
        const user = ctx.switchToHttp().getRequest().user;
        return field ? user?.[field] : user;
      },
    );
    
    @Get('me')
    me(@CurrentUser() user: User, @CurrentUser('email') email: string) {}
    

    To bundle several decorators into one, use applyDecorators:

    export const Auth = (...roles: string[]) =>
      applyDecorators(Roles(roles), UseGuards(AuthGuard, RolesGuard));
    

    ValidationPipe skips arguments from custom parameter decorators unless it is created with validateCustomDecorators: true.

  8. What is an exception filter, and how do you change the error response format?

    An exception filter catches exceptions thrown anywhere in the request lifecycle and writes the response. @Catch(HttpException) limits it to HTTP exceptions; @Catch() with no argument catches everything.

    NestJS
    import {
      ArgumentsHost, Catch, Controller, ExceptionFilter, Get, HttpException,
      HttpStatus, Module, NotFoundException, Param,
    } from '@nestjs/common';
    import { NestFactory } from '@nestjs/core';
    
    @Catch(HttpException)
    class ApiErrorFilter implements ExceptionFilter {
      catch(err: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const req = ctx.getRequest();
        const status = err.getStatus();
        ctx.getResponse().status(status).json({
          ok: false,
          status,
          path: req.url,
          error: err.message,
        });
      }
    }
    
    // GET /books/99
    @Controller('books')
    class BooksController {
      private books = { 1: 'Dune' };
      @Get(':id')
      find(@Param('id') id: string) {
        const title = this.books[id];
        if (!title) throw new NotFoundException(`Book ${id} not found`);
        return { id, title };
      }
      @Get('teapot/brew')
      brew() { throw new HttpException('No coffee here', HttpStatus.I_AM_A_TEAPOT); }
    }
    
    @Module({ controllers: [BooksController] })
    class AppModule {}
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      app.useGlobalFilters(new ApiErrorFilter());
      await app.listen(3000);
    }
    bootstrap();

    The first request answers 404 in the filter's shape: ok, status, path and error, instead of Nest's default statusCode/message/error. GET /books/1 is unaffected, and GET /books/teapot/brew shows the same shape for a 418. Express error handling works the same way one level down: Express error handling.

  9. How does NestJS handle errors you throw in a controller?

    Throw an HttpException or a subclass (BadRequestException, NotFoundException, ForbiddenException...) and Nest's built-in exception filter turns it into a JSON response with that status. Anything else becomes a 500 with a generic message, so internal details never reach the client.

    NestJS
    // /crash fails on purpose: a plain Error becomes a 500
    import { BadRequestException, Controller, Get, HttpException, Module } from '@nestjs/common';
    import { NestFactory } from '@nestjs/core';
    
    // GET /pay
    @Controller()
    class PayController {
      @Get('pay')
      pay() { throw new BadRequestException('Card declined', { description: 'Payment failed' }); }
    
      @Get('code')
      coded() { throw new HttpException('Too many tries', 429, { errorCode: 'RATE_LIMITED' }); }
    
      @Get('crash')
      crash() { throw new Error('database password is hunter2'); }
    }
    
    @Module({ controllers: [PayController] })
    class AppModule {}
    
    async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); }
    bootstrap();

    GET /pay answers 400 with your message and the description as error. Send GET /code: NestJS 12 adds an errorCode option that clients can switch on instead of parsing messages. GET /crash answers 500 with Internal server error, while the real message only goes to the server log.

  10. What is the difference between app.useGlobalGuards() and the APP_GUARD provider?

    Both apply a guard to every route, but app.useGlobalGuards(new AuthGuard()) creates the instance yourself, outside the container, so the guard cannot inject services such as JwtService or Reflector.

    Registering it as a provider lets Nest build it with dependency injection:

    @Module({
      providers: [
        { provide: APP_GUARD, useClass: AuthGuard },
        { provide: APP_PIPE, useValue: new ValidationPipe({ whitelist: true }) },
        { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
        { provide: APP_FILTER, useClass: AllExceptionsFilter },
      ],
    })
    export class AppModule {}
    

    The same pair exists for pipes, interceptors and filters. Prefer the provider form unless the enhancer has no dependencies. One trap: useGlobal* in main.ts is not applied in e2e tests that build the app from the module, while APP_* providers are.

  11. When guards, interceptors and filters are bound at several levels, in what order do they run?

    Guards, interceptors and pipes run from the outside in: global, then controller, then method, and in array order within one decorator. Interceptors unwind in reverse on the way out, like nested functions.

    Exception filters go the other way: Nest tries method filters first, then controller, then global, and only the first filter whose @Catch() matches handles the exception. Filters do not chain; to reuse logic, extend BaseExceptionFilter and call super.catch().

    @UseInterceptors(A)      // controller level
    class C {
      @UseInterceptors(B)    // method level
      @Get() h() {}
    }
    // before: global, A, B -> handler -> after: B, A, global
    
Coddy programming languages illustration

Learn to code with Coddy

Get started