Lesson 16 of 49 advanced

NestJS Architecture Patterns

Building Clean, Scalable Apps

Open interactive version (quiz + challenge)

Real-world analogy

A NestJS app is like a well-organized company. The CEO (AppModule) oversees departments (feature modules). Each department has a receptionist (controller), workers (services), and security (guards). Nobody does someone else's job!

What is it?

NestJS follows a layered architecture: Modules organize features, Controllers handle HTTP, Services contain business logic, and cross-cutting concerns (validation, logging, auth) are handled by Pipes, Guards, Interceptors, and Filters.

Real-world relevance

This architecture pattern (separation of concerns) is used by enterprise companies worldwide. It scales from a 1-person project to a 100-person team without becoming a mess.

Key points

Code example

// The NestJS Request Lifecycle 🔄
// Request → Middleware → Guards → Interceptors (before)
//        → Pipes → Handler → Interceptors (after)
//        → Exception Filters → Response

// 1. Custom Pipe — validate incoming data 🔍
@Injectable()
export class ParseObjectIdPipe implements PipeTransform {
  transform(value: string) {
    if (!Types.ObjectId.isValid(value)) {
      throw new BadRequestException('Invalid ID format');
    }
    return value;
  }
}

// Usage: @Param('id', ParseObjectIdPipe) id: string

// 2. Interceptor — log all requests ⏱️
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const req = context.switchToHttp().getRequest();
    const now = Date.now();
    console.log(`→ ${req.method} ${req.url}`);

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

// 3. Exception Filter — beautiful error responses 🎨
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const response = host.switchToHttp().getResponse();
    const status = exception.getStatus();

    response.status(status).json({
      success: false,
      statusCode: status,
      message: exception.message,
      timestamp: new Date().toISOString(),
    });
  }
}

Line-by-line walkthrough

  1. 1. The NestJS Request Lifecycle 🔄
  2. 2. Request → Middleware → Guards → Interceptors (before)
  3. 3. → Pipes → Handler → Interceptors (after)
  4. 4. → Exception Filters → Response
  5. 5.
  6. 6. 1. Custom Pipe — validate incoming data 🔍
  7. 7. Decorator that adds metadata or behavior
  8. 8. Exporting for use in other files
  9. 9.
  10. 10. Conditional check
  11. 11. Throwing an error
  12. 12. Closing block
  13. 13. Returning a value
  14. 14. Closing block
  15. 15. Closing block
  16. 16.
  17. 17. Usage: @Param('id', ParseObjectIdPipe) id: string
  18. 18.
  19. 19. 2. Interceptor — log all requests ⏱️
  20. 20. Decorator that adds metadata or behavior
  21. 21. Exporting for use in other files
  22. 22.
  23. 23. Declaring a variable
  24. 24. Declaring a variable
  25. 25. Printing output to the console
  26. 26.
  27. 27. Returning a value
  28. 28.
  29. 29. Closing expression
  30. 30. Closing block
  31. 31. Closing block
  32. 32.
  33. 33. 3. Exception Filter — beautiful error responses 🎨
  34. 34. Decorator that adds metadata or behavior
  35. 35. Exporting for use in other files
  36. 36. Catching any errors from the try block
  37. 37. Declaring a variable
  38. 38. Declaring a variable
  39. 39.
  40. 40.
  41. 41.
  42. 42.
  43. 43.
  44. 44.
  45. 45.
  46. 46. Closing block
  47. 47. Closing block

Spot the bug

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    console.log("Before...");
    return next.handle();
    console.log("After...");
  }
}
Need a hint?
Can code after a return statement ever execute?
Show answer
The console.log('After...') after return will never run. To log after the handler, use RxJS: return next.handle().pipe(tap(() => console.log('After...')));

Explain like I'm 5

Think of a NestJS app like a school. The principal (AppModule) runs everything. Each classroom (module) has a teacher who talks to students (controller) and a helper who does the work (service). Guards are hall monitors checking passes before you enter!

Fun fact

The NestJS request lifecycle has 7 distinct layers. A single request goes through: Middleware → Guards → Interceptors → Pipes → Handler → Interceptors → Filters. It's like airport security with 7 checkpoints! ✈️

Hands-on challenge

Create a custom LoggingInterceptor that logs the method, URL, and response time for every request. Apply it globally with `app.useGlobalInterceptors()`.

More resources

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