# NestJS Interview: Guards, Interceptors and Modular Architecture > Common NestJS interview questions on Guards, Interceptors and modular architecture, with concrete TypeScript code examples and technical explanations. - Published: 2026-03-30 - Updated: 2026-03-30 - Author: SharpSkill - Tags: nestjs, guards, interceptors, architecture, interview - Reading time: 9 min --- NestJS technical interviews consistently focus on three framework pillars: Guards, Interceptors, and modular architecture. These mechanisms form the backbone of any well-structured NestJS application, and hiring managers use them to assess real-world framework proficiency. > **What interviewers evaluate** > > A Guard controls access (who gets in), an Interceptor transforms data (what goes in and out), and modular architecture organizes everything. Mastering all three demonstrates deep NestJS understanding beyond basic CRUD operations. ## How NestJS Guards work and common interview scenarios Guards implement the `CanActivate` interface and determine whether a request reaches the handler. Unlike middleware, Guards access the NestJS execution context (`ExecutionContext`), enabling authorization decisions based on handler metadata. Classic question: "Implement a Guard that checks user roles." ```typescript // roles.guard.ts import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { // Retrieve roles defined via the @Roles() decorator const requiredRoles = this.reflector.getAllAndOverride('roles', [ context.getHandler(), context.getClass(), ]); // No roles required means the route is public if (!requiredRoles) return true; // Extract user from the HTTP request const { user } = context.switchToHttp().getRequest(); // Check if the user holds at least one required role return requiredRoles.some((role) => user.roles?.includes(role)); } } ``` The `Reflector` reads metadata attached by custom decorators. The companion `@Roles()` decorator looks like this: ```typescript // roles.decorator.ts import { SetMetadata } from '@nestjs/common'; // Creates a decorator that attaches roles as metadata export const Roles = (...roles: string[]) => SetMetadata('roles', roles); ``` A strong interview answer goes beyond the code. The execution order matters: Middleware → Guards → Interceptors (before) → Pipes → Handler → Interceptors (after) → Exception Filters. This sequence appears in virtually every NestJS interview. ## NestJS Interceptors: transforming requests and responses Interceptors implement `NestInterceptor` and use RxJS to manipulate the data stream before and after the handler. Their power comes from access to `CallHandler`, which represents the execution pipeline. Frequent question: "Create a logging Interceptor that measures execution time." ```typescript // logging.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger(LoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const { method, url } = request; const now = Date.now(); // next.handle() invokes the route handler return next.handle().pipe( tap(() => { // Runs AFTER the handler responds const duration = Date.now() - now; this.logger.log(`${method} ${url} — ${duration}ms`); }), ); } } ``` The RxJS `tap` operator observes the stream without modifying it. To transform the response, `map` is the right operator: ```typescript // transform.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; // Generic interface for a standardized API response interface ApiResponse { data: T; timestamp: string; path: string; } @Injectable() export class TransformInterceptor implements NestInterceptor> { intercept(context: ExecutionContext, next: CallHandler): Observable> { const request = context.switchToHttp().getRequest(); return next.handle().pipe( map((data) => ({ data, timestamp: new Date().toISOString(), path: request.url, })), ); } } ``` This standardized response pattern scores well in interviews: it shows the ability to think about API design and response consistency. > **Guard vs Interceptor vs Middleware** > > Middleware: raw HTTP processing (like Express). Guard: boolean access decision. Interceptor: request/response stream transformation with RxJS. Confusing these three concepts in an interview is an immediate red flag. ## NestJS modular architecture: building scalable applications Modular architecture separates junior from senior developers in interviews. NestJS enforces module-based organization, but the real questions target architectural decisions: when to create a module, and how to manage inter-module dependencies. Advanced question: "How would you structure an e-commerce NestJS application with properly decoupled modules?" ```typescript // orders/orders.module.ts import { Module } from '@nestjs/common'; import { OrdersService } from './orders.service'; import { OrdersController } from './orders.controller'; import { PaymentsModule } from '../payments/payments.module'; import { ProductsModule } from '../products/products.module'; @Module({ // Import modules that OrdersModule depends on imports: [PaymentsModule, ProductsModule], controllers: [OrdersController], providers: [OrdersService], // Expose OrdersService to modules that import OrdersModule exports: [OrdersService], }) export class OrdersModule {} ``` The classic interview trap: circular dependencies. If `OrdersModule` imports `PaymentsModule` and `PaymentsModule` imports `OrdersModule`, NestJS fails at startup. The escape hatch uses `forwardRef`: ```typescript // Resolving circular dependencies @Module({ imports: [forwardRef(() => PaymentsModule)], }) export class OrdersModule {} ``` The strongest interview answer explains that `forwardRef` is a workaround, not a solution. The proper fix is refactoring to extract a shared module (`SharedOrderPaymentModule`) or using an Event Emitter to decouple communications. ## Dynamic Modules and advanced configuration Dynamic Modules create configurable, reusable modules across projects. This pattern appears in senior-level interviews. Question: "Create a configurable cache module with dynamic options." ```typescript // cache/cache.module.ts import { Module, DynamicModule, Global } from '@nestjs/common'; import { CacheService } from './cache.service'; // Module configuration interface export interface CacheModuleOptions { ttl: number; // Time-to-live in seconds maxItems: number; // Maximum number of entries prefix: string; // Key prefix } @Global() // Available everywhere without explicit import @Module({}) export class CacheModule { static forRoot(options: CacheModuleOptions): DynamicModule { return { module: CacheModule, providers: [ { // Inject options via a custom token provide: 'CACHE_OPTIONS', useValue: options, }, CacheService, ], exports: [CacheService], }; } } ``` The corresponding service injects options via `@Inject()`: ```typescript // cache/cache.service.ts import { Injectable, Inject } from '@nestjs/common'; import { CacheModuleOptions } from './cache.module'; @Injectable() export class CacheService { private store = new Map(); constructor(@Inject('CACHE_OPTIONS') private options: CacheModuleOptions) {} set(key: string, value: any): void { const prefixedKey = `${this.options.prefix}:${key}`; this.store.set(prefixedKey, { value, expiry: Date.now() + this.options.ttl * 1000, }); } get(key: string): T | null { const prefixedKey = `${this.options.prefix}:${key}`; const entry = this.store.get(prefixedKey); // Check expiration before returning if (!entry || entry.expiry < Date.now()) return null; return entry.value as T; } } ``` The `forRoot` / `forRootAsync` pattern is a NestJS staple. `forRootAsync` loads configuration from an injectable `ConfigService`, which is the recommended production practice. > **Interview trap: @Global()** > > Marking a module `@Global()` seems convenient, but overusing it creates invisible coupling. Mentioning in an interview that `@Global()` should be reserved for cross-cutting services (config, cache, logger) demonstrates architectural maturity. ## Custom Decorators and Guard composition Custom decorators combine multiple Guards and metadata into a single annotation. This topic comes up for mid-senior positions. ```typescript // auth.decorator.ts import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common'; import { JwtAuthGuard } from './jwt-auth.guard'; import { RolesGuard } from './roles.guard'; // Combines JWT authentication + role verification export function Auth(...roles: string[]) { return applyDecorators( SetMetadata('roles', roles), UseGuards(JwtAuthGuard, RolesGuard), ); } ``` Usage in a controller: ```typescript // orders/orders.controller.ts import { Controller, Get, Post, Body } from '@nestjs/common'; import { Auth } from '../auth/auth.decorator'; import { OrdersService } from './orders.service'; @Controller('orders') export class OrdersController { constructor(private ordersService: OrdersService) {} @Get() @Auth('admin', 'manager') // Single decorator replaces UseGuards + Roles findAll() { return this.ordersService.findAll(); } @Post() @Auth('admin') create(@Body() dto: CreateOrderDto) { return this.ordersService.create(dto); } } ``` The `applyDecorators` pattern simplifies controller code and centralizes authorization logic. Proactively suggesting this approach in an interview is a strong signal. ## Conclusion - Guards use `CanActivate` and the `Reflector` for access decisions based on handler metadata - Interceptors leverage RxJS (`tap`, `map`) to transform the stream before and after the handler - The execution order Middleware → Guards → Interceptors → Pipes → Handler is a near-universal question - Circular dependencies should be resolved through architectural refactoring, not `forwardRef` - Dynamic Modules (`forRoot`/`forRootAsync`) demonstrate the ability to build reusable components - `applyDecorators` composes multiple Guards into a single, readable decorator - Practice these questions with real code on [NestJS modules](/technologies/node-nestjs/interview-questions/nestjs-modules-di) and [Interceptors](/technologies/node-nestjs/interview-questions/middleware-interceptors) --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/node-nestjs/nestjs-guards-interceptors-modular-architecture