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.

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.
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."
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<string[]>('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:
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."
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<any> {
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:
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<T> {
data: T;
timestamp: string;
path: string;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
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.
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?"
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:
// 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.
Ready to ace your Node.js / NestJS interviews?
Practice with our interactive simulators, flashcards, and technical tests.
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."
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():
import { Injectable, Inject } from '@nestjs/common';
import { CacheModuleOptions } from './cache.module';
@Injectable()
export class CacheService {
private store = new Map<string, { value: any; expiry: number }>();
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<T>(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.
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.
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:
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
CanActivateand theReflectorfor 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 applyDecoratorscomposes multiple Guards into a single, readable decorator- Practice these questions with real code on NestJS modules and Interceptors
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

NestJS + Prisma: The Modern Backend Stack for Node.js
Complete guide to building a modern backend API with NestJS and Prisma. Setup, models, services, transactions and best practices explained.

Node.js Backend Interview Questions: Complete Guide 2026
The 25 most common Node.js backend interview questions. Event loop, async/await, streams, clustering and performance explained with detailed answers.

NestJS: Building a Complete REST API
Complete guide to building a professional REST API with NestJS. Controllers, Services, Modules, validation with class-validator and error handling explained.