Lesson 44 of 49 intermediate

NestJS Middleware & Interceptors

Processing Requests Before and After

Open interactive version (quiz + challenge)

Real-world analogy

Middleware is like a toll booth on a highway — EVERY car passes through it before reaching the city. Interceptors are like a car wash with a before-and-after photo — they snap a picture when you drive in (before handler) and another when you drive out (after handler). Both process your car, but at different stages of the trip!

What is it?

Middleware processes requests before they reach route handlers, while interceptors wrap around handlers to process both the request and response. Together they form a powerful pipeline for logging, transformation, caching, and cross-cutting concerns without cluttering your controllers.

Real-world relevance

Every production API uses middleware and interceptors. Morgan/Winston for logging, helmet for security headers, compression for response gzip, rate-limiter for throttling — all are middleware. Response wrappers, timing metrics, and cache layers are interceptors.

Key points

Code example

// 1. Logging Middleware — runs on every request
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  private logger = new Logger('HTTP');

  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl } = req;
    const start = Date.now();

    res.on('finish', () => {
      const duration = Date.now() - start;
      const { statusCode } = res;
      this.logger.log(
        `${method} ${originalUrl} ${statusCode} — ${duration}ms`,
      );
    });

    next(); // Don't forget this!
  }
}

// Apply middleware in module
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';

@Module({ controllers: [UsersController], providers: [UsersService] })
export class UsersModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*'); // Apply to all routes
    // Or target specific routes:
    // .forRoutes({ path: 'users', method: RequestMethod.GET })
    // .exclude({ path: 'health', method: RequestMethod.GET })
  }
}

// 2. Response Transform Interceptor — wraps all responses
import {
  Injectable, NestInterceptor,
  ExecutionContext, CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

// 3. Timing Interceptor — measure response time
@Injectable()
export class TimingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    const req = context.switchToHttp().getRequest();

    return next.handle().pipe(
      tap(() => {
        console.log(
          `${req.method} ${req.url} — ${Date.now() - now}ms`,
        );
      }),
    );
  }
}

// Apply globally in main.ts
// app.useGlobalInterceptors(new TransformInterceptor());

// Or per-controller with decorator
// @UseInterceptors(TransformInterceptor)

Line-by-line walkthrough

  1. 1. 1. Logging Middleware — runs on every request
  2. 2. Importing NestJS classes and Logger
  3. 3. Importing Express types
  4. 4.
  5. 5. Injectable class implementing NestMiddleware
  6. 6. Create a logger with 'HTTP' context label
  7. 7.
  8. 8. The middleware function — req, res, next
  9. 9. Destructure method and URL from request
  10. 10. Record the start time
  11. 11.
  12. 12. When the response finishes sending...
  13. 13. Calculate how long it took
  14. 14. Get the status code
  15. 15. Log the formatted message
  16. 16. Format: GET /users 200 — 15ms
  17. 17. Close the log call
  18. 18. Close the finish listener
  19. 19.
  20. 20. IMPORTANT: call next() to continue!
  21. 21. Close the middleware
  22. 22. Close the class
  23. 23.
  24. 24. Apply middleware in module
  25. 25. Import Module and middleware types
  26. 26.
  27. 27. Module decorator with providers
  28. 28. Implement NestModule interface
  29. 29. Configure method receives MiddlewareConsumer
  30. 30. Start building the middleware chain
  31. 31. Apply LoggerMiddleware
  32. 32. To all routes
  33. 33. Alternative: target specific routes
  34. 34. Or exclude certain routes
  35. 35. Close configure
  36. 36. Close class
  37. 37.
  38. 38. 2. Response Transform Interceptor
  39. 39. Import interceptor types
  40. 40.
  41. 41.
  42. 42.
  43. 43. Import RxJS Observable
  44. 44. Import the map operator
  45. 45.
  46. 46. Injectable interceptor class
  47. 47. Intercept method with context and next handler
  48. 48. Call the handler and pipe the result
  49. 49. Map transforms the response data
  50. 50. Wrap in standard format
  51. 51. success flag
  52. 52. Original data
  53. 53. Add timestamp
  54. 54. Close map
  55. 55. Close pipe
  56. 56. Close intercept
  57. 57. Close class
  58. 58.
  59. 59. 3. Timing Interceptor
  60. 60. Another injectable interceptor
  61. 61. Intercept method
  62. 62. Record start time
  63. 63. Get the request object
  64. 64.
  65. 65. Call handler and tap the result
  66. 66. tap runs side effects without changing data
  67. 67. Log the method, URL, and duration
  68. 68. Format the timing output
  69. 69. Close tap
  70. 70. Close pipe
  71. 71. Close intercept
  72. 72. Close class
  73. 73.
  74. 74. Apply globally in main.ts
  75. 75.
  76. 76. Or per-controller with decorator

Spot the bug

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const token = req.headers['authorization'];
    if (!token) {
      res.status(401).json({ message: 'Unauthorized' });
    }
    req['user'] = verifyToken(token);
    next();
  }
}
Need a hint?
What happens when there's no token but the code keeps executing?
Show answer
When token is missing, res.status(401).json() sends a response BUT next() still gets called, causing the handler to run AND potentially sending a second response (crash!). Fix: add 'return' before res.status(401) to stop execution: `return res.status(401).json(...);

Explain like I'm 5

Imagine you're going to a friend's birthday party. Middleware is like the person at the door checking invitations — they see everyone who comes in. Interceptors are like gift wrapping — they wrap your present (data) nicely before you give it, and they unwrap the thank-you card (response) after. Both help, but at different times!

Fun fact

NestJS interceptors use RxJS (Reactive Extensions for JavaScript). RxJS was created by Microsoft and is the same library used by Angular. The pipe/map/tap pattern you see in interceptors is the same pattern used in Angular HTTP calls. Learning it once pays off in both frameworks!

Hands-on challenge

Create a LoggerMiddleware that logs method, URL, status code, and response time for every request. Then create a TransformInterceptor that wraps all responses in `{ success: true, data: ..., requestId: uuid() }`. Apply both globally and test them!

More resources

Open interactive version (quiz + challenge) ← Back to course: Full-Stack Playbook