30 NestJS Interview Questions and Answers (2026)
30 NestJS interview questions with full answers: modules, DI, guards, pipes, interceptors, JWT auth, microservices, and testing. Updated for 2026.
On this page
NestJS has become the default choice for backend teams that want structure, testability, and TypeScript from day one. It shows up constantly on job postings for backend engineers, full-stack developers, and API architects. The framework borrows Angular's module system and dependency injection model and brings them to Node.js, which makes it powerful, but it also means interviews probe architectural thinking, not just syntax.
These 30 questions reflect what interviewers are actually asking in 2025 and 2026, from foundational concepts to microservices and testing. The answers are written to be said out loud in an interview: clear, specific, and backed by real code examples you can reference if you're asked to write something on a whiteboard or shared editor.
If you're setting up a fresh NestJS project to practice these concepts in, getting your npm scripts and Node.js tooling right first means you're not fighting build configuration while you're trying to demo dependency injection and module structure.
Category 1: Architecture and Core Concepts (Q1-Q8)
These questions establish whether you understand what NestJS adds on top of Node.js and Express, and whether you can reason about its architecture rather than just its syntax. They are almost always the opening questions in a NestJS interview.
Q1. What is NestJS and how does it differ from plain Express?
NestJS is a TypeScript-first Node.js framework for building scalable server-side applications. It is built on top of Express by default (or Fastify as an alternative) and adds a full architectural layer: modules, dependency injection, decorators, and a clear separation between controllers, services, and providers.
Plain Express is minimal by design. It gives you routing and middleware and leaves every architectural decision to you. That works fine for small apps but creates inconsistency across teams at scale. NestJS solves this by enforcing a module-based structure inspired by Angular: every feature is a self-contained module, dependencies are injected rather than imported and instantiated directly, and the result is code that's easier to test, easier to refactor, and consistent across the entire codebase.
The practical difference: in Express you write a route handler and manage all of its dependencies manually. In NestJS you define a controller with decorators, inject a service, and the framework wires everything together for you.
- Built on Express (default) or Fastify, with the same underlying HTTP handling
- Adds modules, decorators, and a dependency injection container on top
- Enforces a consistent project structure across teams and feature areas
- First-class TypeScript support, where Express is JavaScript-first with types bolted on
- A built-in testing module that makes mocking dependencies straightforward
Q2. What is a Module in NestJS and what does it contain?
A module is the basic building block of a NestJS application. Every application has at least one module, the root AppModule. Modules use the @Module() decorator and declare four things: imports (other modules whose exported providers this module needs), controllers (the request handlers belonging to this module), providers (services, repositories, guards, and other injectables), and exports (providers this module makes available to other modules that import it).
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService],
})
export class UsersModule {}Modules enforce encapsulation. A provider declared in UsersModule is not available anywhere else unless it's exported. This prevents unintended coupling between features and is one of the reasons large NestJS codebases stay maintainable as they grow.
Q3. What is the difference between a Controller and a Provider?
A Controller handles incoming HTTP requests and returns responses. It maps routes to handler methods using decorators like @Get(), @Post(), @Put(), and @Delete(). Controllers should contain no business logic: they receive input, call a service, and return the result.
A Provider is anything injectable: a service, repository, guard, interceptor, or factory. Services contain the business logic. They're marked with @Injectable() and injected into controllers or other services through the constructor.
// Controller: handles routing only
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
}
// Service: contains business logic
@Injectable()
export class UsersService {
async findOne(id: string): Promise<User> {
return this.usersRepository.findById(id);
}
}Q4. How does Dependency Injection work in NestJS?
NestJS uses an IoC (Inversion of Control) container managed at the module level. When you declare a provider in a module's providers array, NestJS registers it in the container. When a controller or service declares a dependency in its constructor, NestJS resolves and injects it automatically.
@Injectable()
export class UsersService {
constructor(private readonly mailerService: MailerService) {}
}You never call new MailerService() anywhere. NestJS creates the instance, manages its lifecycle, and injects it wherever it's needed. Providers are singleton by default within a module, meaning one instance is shared across all consumers.
This pattern makes testing straightforward: you can replace any dependency with a mock without changing the class that uses it, which is exactly what Q28 and Q29 in this guide cover.
Q5. What are the different types of Providers in NestJS?
NestJS supports four provider types, and knowing when to reach for each one is a strong signal of real architectural understanding.
| Provider type | What it does | Example |
|---|---|---|
| Standard (class) | The most common. A class decorated with @Injectable() registered directly | providers: [UsersService] (shorthand for { provide: UsersService, useClass: UsersService }) |
| Value | Injects a constant or existing object | { provide: 'API_KEY', useValue: process.env.API_KEY } |
| Factory | Uses a factory function; supports async creation and other injected dependencies | { provide: 'DB_CONNECTION', useFactory: async (config) => createConnection(...), inject: [ConfigService] } |
| Existing (alias) | Creates an alias for an existing provider | { provide: 'LEGACY_SERVICE', useExisting: UsersService } |
Use factory providers when initialization is asynchronous or when the provider depends on runtime configuration, such as a database connection string that comes from ConfigService.
Q6. What are Decorators in NestJS and how are they used?
Decorators are TypeScript functions that attach metadata to classes, methods, parameters, or properties. NestJS uses them everywhere to configure behavior without imperative setup code.
- @Module(): defines a module's metadata
- @Controller('route'): marks a class as a controller with a base route
- @Injectable(): marks a class as injectable through DI
- @Get(), @Post(), @Put(), @Delete(): HTTP method decorators on handler methods
- @Param(), @Body(), @Query(), @Headers(): extract parts of the request
- @UseGuards(), @UseInterceptors(), @UsePipes(): attach pipeline components
You can also create custom decorators to extract reusable logic, which is a common follow-up question once you've named the built-in ones.
// Custom decorator to extract the current user from the request
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
// Usage in a controller
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}Q7. What is the NestJS request lifecycle (execution order)?
This is one of the most commonly asked NestJS interview questions, and it's the one this guide's research found almost no competing article explains clearly. The full execution order for an incoming request is:
- 1
Middleware
Runs first. Has access to req, res, and next(). Used for logging, body parsing, CORS, and session handling. Cannot block requests based on business logic.
- 2
Guards
Run after middleware. Determine whether the request is allowed to proceed. Return true to allow, false or throw to deny. Used for authentication and authorization.
- 3
Interceptors (before handler)
Run after guards. Can transform the incoming request or add behavior before the handler executes.
- 4
Pipes
Run after interceptors. Validate and transform incoming data: route params, request body, and query params.
- 5
Route Handler
The actual controller method executes and produces a result.
- 6
Interceptors (after handler)
Wrap the response. Can transform the response, add headers, cache results, or log execution time.
- 7
Exception Filters
Catch any exception thrown at any point in the pipeline and return a formatted error response.
Q8. How do you resolve circular dependencies in NestJS?
A circular dependency occurs when Module A imports Module B and Module B imports Module A, or when Service A depends on Service B and Service B depends on Service A. NestJS cannot resolve these at startup and will throw an error.
The correct fix is architectural: refactor so the shared logic lives in a third SharedModule that both A and B import. This is almost always the right answer, and it's the answer that shows you think about dependency graphs rather than reaching for a quick patch.
If you genuinely cannot avoid the circular reference, NestJS provides forwardRef() as an escape hatch:
// In Service A
@Injectable()
export class ServiceA {
constructor(
@Inject(forwardRef(() => ServiceB))
private serviceB: ServiceB,
) {}
}
// In Service B
@Injectable()
export class ServiceB {
constructor(
@Inject(forwardRef(() => ServiceA))
private serviceA: ServiceA,
) {}
}For modules, you apply forwardRef() in the imports array of both modules involved.
Category 2: Request Pipeline (Q9-Q16)
This category goes deep on the layers introduced in Q7's lifecycle: middleware, guards, pipes, interceptors, and exception filters. Expect interviewers to probe not just what each layer does, but when you'd reach for one over another, since that distinction is where junior and senior answers diverge most.
Q9. What is Middleware in NestJS and when do you use it?
Middleware in NestJS is identical to Express middleware: a function that runs before the route handler, with access to req, res, and next(). It's applied at the module level using configure() and the MiddlewareConsumer.
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.url}`);
next();
}
}
// Apply in a module
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*');
}
}Use middleware for logging, request tracing, body parsing, CORS, rate limiting, and session management. Don't use it for authentication or authorization, that belongs in Guards. Middleware also runs before NestJS's DI-aware pipeline, so it can't inject providers as cleanly as the layers that follow it.
Q10. What are Guards and how do they work?
Guards implement the CanActivate interface and return a boolean (or a Promise or Observable that resolves to one). If a guard returns true, the request proceeds. If it returns false or throws an UnauthorizedException, the request is blocked.
Guards have access to the ExecutionContext, which provides request metadata, the handler being called, and the class it belongs to. That's what makes them suitable for both authentication (is this a valid user?) and authorization (does this user have permission?).
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(' ')[1];
if (!token) return false;
try {
request.user = this.jwtService.verify(token);
return true;
} catch {
throw new UnauthorizedException();
}
}
}
// Apply to a single route
@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}Q11. What are Pipes and what are the two main uses?
Pipes implement PipeTransform and run on the value of a route parameter, body, or query string before it reaches the handler. They serve two purposes: transformation (convert a string id to a number) and validation (reject input that doesn't match a schema).
Built-in pipes include ValidationPipe, ParseIntPipe, ParseUUIDPipe, ParseBoolPipe, DefaultValuePipe, and ParseArrayPipe. ValidationPipe paired with class-validator is the standard pattern for DTO validation:
// DTO
import { IsString, IsEmail, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@MinLength(8)
password: string;
}
// Controller: ValidationPipe transforms and validates the body
@Post()
@UsePipes(new ValidationPipe({ whitelist: true }))
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}Q12. What are Interceptors and what can they do?
Interceptors implement NestInterceptor. They wrap the route handler's execution using RxJS Observables, which lets them add behavior both before and after the handler runs.
- Logging request duration
- Transforming the response format
- Caching responses
- Adding headers
- Handling timeouts
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
return next.handle().pipe(
tap(() => {
const duration = Date.now() - start;
console.log(`Request took ${duration}ms`);
}),
);
}
}
// Response transformation interceptor
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({ success: true, data })),
);
}
}Apply interceptors globally in main.ts with app.useGlobalInterceptors(new LoggingInterceptor()), or per-route with @UseInterceptors().
Q13. What are Exception Filters and when do you need a custom one?
Exception filters catch exceptions thrown during request processing and transform them into HTTP responses. NestJS includes a global exception filter that handles HttpException and its subclasses; any unrecognized exception returns a 500 Internal Server Error.
You write a custom exception filter when you need consistent error response formatting, want to log errors to an external service, or need to handle exceptions thrown by third-party libraries, like TypeORM's EntityNotFoundError.
@Catch(EntityNotFoundError)
export class EntityNotFoundFilter implements ExceptionFilter {
catch(exception: EntityNotFoundError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
response.status(404).json({
statusCode: 404,
message: 'Resource not found',
error: 'Not Found',
});
}
}@Catch() called without arguments catches every exception, which is the pattern you'd reach for to build a single global error formatter for an API.
Q14. What is the difference between Guards, Middleware, and Interceptors?
This is one of the most common NestJS interview questions, and the cleanest way to answer it is to anchor each one to what it has access to and what it's best used for.
| Middleware | Guards | |
|---|---|---|
| Runs | Before the NestJS pipeline, identical to Express middleware | Inside the NestJS DI-aware pipeline, after middleware |
| Context access | req, res, next() only; no knowledge of which handler will run | Full ExecutionContext: knows the controller and handler being called |
| Best for | Logging, CORS, body parsing, sessions | Authentication, authorization, role checks |
| Returns | Calls next() to continue, or ends the response | Boolean (or Promise/Observable<boolean>) to allow or deny |
Interceptors sit on the other side of the handler from guards: they wrap execution using RxJS, so they can access both the incoming request and the outgoing response. They're the right tool for logging request duration, transforming responses, caching, and adding headers, anything that needs to wrap rather than gate the request.
The summary worth memorizing: middleware is for cross-cutting concerns outside the DI system, guards are for access control, and interceptors are for wrapping execution and transforming data flow.
Q15. How do you create and apply a custom Pipe for UUID validation?
This question checks whether you understand the PipeTransform interface well enough to implement it yourself, not just configure the built-in pipes.
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import { validate as isUUID } from 'uuid';
@Injectable()
export class ParseUUIDPipe implements PipeTransform<string> {
transform(value: string): string {
if (!isUUID(value)) {
throw new BadRequestException(`${value} is not a valid UUID`);
}
return value;
}
}
// Usage in controller
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.findOne(id);
}NestJS already ships a built-in ParseUUIDPipe, and it's worth saying so out loud. But writing a custom one in an interview demonstrates that you understand the interface underneath it, which is the actual point of the question.
Q16. What is the difference between @Param(), @Body(), @Query(), and @Headers()?
These are parameter decorators that extract specific parts of the incoming request directly into your handler's arguments, instead of you reaching into req manually.
| Decorator | Extracts | Example |
|---|---|---|
| @Param('id') | A route parameter | For /users/:id, @Param('id') gives you the id value |
| @Body() | The entire request body (or a single field with @Body('email')) | Used with DTOs and ValidationPipe |
| @Query('page') | A query string parameter | For /users?page=2, @Query('page') gives you '2' |
| @Headers('authorization') | A request header | Returns the raw header value as a string |
@Put(':id')
update(
@Param('id', ParseUUIDPipe) id: string,
@Body() updateUserDto: UpdateUserDto,
@Query('notify') notify: string,
@Headers('x-request-id') requestId: string,
) {
return this.usersService.update(id, updateUserDto);
}Category 3: Authentication and Security (Q17-Q20)
Once an interviewer is convinced you understand the request pipeline, they'll usually move into how you secure it. These four questions cover the patterns that come up in almost every backend role: token-based auth, role checks, configuration, and the broader security checklist.
Q17. How do you implement JWT authentication in NestJS?
The standard approach uses @nestjs/passport and @nestjs/jwt with the passport-jwt strategy. It comes together in three steps: configure JwtModule, create a strategy that validates the token, and guard the routes that need protection.
- 1
Configure JwtModule
typescript — auth.module.ts@Module({ imports: [ JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (config: ConfigService) => ({ secret: config.get<string>('JWT_SECRET'), signOptions: { expiresIn: '1h' }, }), }), ], providers: [AuthService, JwtStrategy], exports: [AuthService], }) export class AuthModule {} - 2
Create the JWT strategy
typescript — jwt.strategy.ts@Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(config: ConfigService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: config.get<string>('JWT_SECRET'), }); } async validate(payload: { sub: string; email: string }) { return { userId: payload.sub, email: payload.email }; } } - 3
Guard the protected routes
typescript@UseGuards(AuthGuard('jwt')) @Get('profile') getProfile(@Request() req) { return req.user; // populated by JwtStrategy.validate() }The validate() method's return value is attached to req.user automatically. Always store the JWT secret in environment variables, never in source code, which is exactly what Q19 covers next.
Q18. How do you implement role-based access control (RBAC) in NestJS?
RBAC requires two things: attaching role metadata to routes, and checking that metadata inside a Guard. NestJS's Reflector class is what bridges the two.
- 1
Create a Roles decorator with SetMetadata
typescriptexport const Roles = (...roles: string[]) => SetMetadata('roles', roles); - 2
Create a RolesGuard that reads the metadata
typescript — roles.guard.ts@Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<string[]>( 'roles', [context.getHandler(), context.getClass()], ); if (!requiredRoles) return true; const { user } = context.switchToHttp().getRequest(); return requiredRoles.some((role) => user.roles?.includes(role)); } } - 3
Apply both guards together
typescript@UseGuards(AuthGuard('jwt'), RolesGuard) @Roles('admin') @Delete(':id') remove(@Param('id') id: string) { return this.usersService.remove(id); }The JWT guard runs first and populates req.user. The RolesGuard then checks whether req.user.roles includes the required role, and the order in @UseGuards() matters precisely because of the request lifecycle covered in Q7.
Q19. How do you manage environment configuration in NestJS?
Use @nestjs/config, which wraps dotenv and integrates with the DI system so you can inject configuration values like any other provider.
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // available in every module without re-importing
envFilePath: '.env',
validationSchema: Joi.object({
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().min(32).required(),
PORT: Joi.number().default(3000),
}),
}),
],
})
export class AppModule {}
// Inject in any service
@Injectable()
export class AppService {
constructor(private config: ConfigService) {}
getDatabaseUrl(): string {
return this.config.get<string>('DATABASE_URL');
}
}The validationSchema option with Joi validates required variables at startup. If DATABASE_URL is missing, the app crashes immediately with a clear error rather than failing silently the first time someone hits an endpoint that needs it.
Q20. How do you prevent common security vulnerabilities in a NestJS API?
This question is really asking whether you think about security as a set of layers rather than a single checkbox. A strong answer names each layer and the specific tool or pattern that covers it.
- Helmet: sets security-related HTTP headers like X-Content-Type-Options and X-Frame-Options: app.use(helmet())
- CORS: configure allowed origins explicitly rather than allowing all: app.enableCors({ origin: process.env.ALLOWED_ORIGINS.split(',') })
- Rate limiting: use @nestjs/throttler to prevent brute-force attacks: ThrottlerModule.forRoot({ ttl: 60, limit: 10 })
- Input validation: always use ValidationPipe with whitelist: true and forbidNonWhitelisted: true, which strips unknown properties and rejects requests with unexpected fields
- SQL injection: use TypeORM's query builder with parameterized queries, never raw string interpolation
- JWT storage: instruct clients to store tokens in HttpOnly cookies, not localStorage, to prevent theft through XSS
Category 4: Database Integration (Q21-Q24)
Almost every NestJS role involves a database, so interviewers use this category to check whether you can wire up an ORM, manage relations, and reason about transactions, not just write a findOne() call. If you want a deeper comparison of ORM tooling beyond what NestJS ships with by default, this practical Drizzle ORM migrations guide is a useful companion read.
Q21. How do you integrate TypeORM with NestJS?
TypeORM integration follows a consistent pattern: configure the connection at the root, declare entities, register them per feature module, and inject repositories into services.
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService): TypeOrmModuleOptions => ({
type: 'postgres',
url: config.get('DATABASE_URL'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false, // never true in production
migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
migrationsRun: true,
}),
}),
],
})
export class AppModule {}
// Entity
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
passwordHash: string;
@CreateDateColumn()
createdAt: Date;
}
// Feature module registers the entity for injection
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
})
export class UsersModule {}
// Service injects the repository
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findByEmail(email: string): Promise<User | null> {
return this.usersRepository.findOne({ where: { email } });
}
}Q22. How does Mongoose integrate with NestJS?
Mongoose integration mirrors TypeORM's pattern but swaps entities and repositories for schemas and models. If you're asked to compare the two, this is the structural symmetry to point out.
// app.module.ts
MongooseModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
uri: config.get('MONGODB_URI'),
}),
}),
// Schema
@Schema({ timestamps: true })
export class User extends Document {
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true })
passwordHash: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
// Feature module
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])
// Service
@Injectable()
export class UsersService {
constructor(@InjectModel(User.name) private userModel: Model<User>) {}
async findByEmail(email: string): Promise<User | null> {
return this.userModel.findOne({ email }).exec();
}
}Q23. How do you handle database transactions in NestJS with TypeORM?
TypeORM gives you two levels of control. The QueryRunner API is explicit: you connect, start the transaction, run your operations, and commit or roll back yourself.
async transferFunds(senderId: string, receiverId: string, amount: number) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.decrement(Account, { id: senderId }, 'balance', amount);
await queryRunner.manager.increment(Account, { id: receiverId }, 'balance', amount);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}For simpler cases, DataSource.transaction() wraps the same commit and rollback behavior automatically, which is the version worth reaching for unless you specifically need the manual control:
await this.dataSource.transaction(async (manager) => {
await manager.save(Order, newOrder);
await manager.update(Inventory, { productId }, { stock: () => 'stock - 1' });
});Q24. How do you define TypeORM relations (One-to-Many, Many-to-Many)?
Relations in TypeORM are declared with decorators on both sides of the relationship. A One-to-Many relation, like one User having many Posts, pairs @OneToMany on the parent with @ManyToOne and @JoinColumn on the child:
// One-to-Many: one User has many Posts
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
}
@Entity()
export class Post {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'author_id' })
author: User;
}
// Many-to-Many: Posts and Tags
@Entity()
export class Post {
@ManyToMany(() => Tag, (tag) => tag.posts)
@JoinTable() // only one side declares @JoinTable
tags: Tag[];
}
@Entity()
export class Tag {
@ManyToMany(() => Post, (post) => post.tags)
posts: Post[];
}- Use eager: true on a relation to always load it automatically with its parent
- Use lazy loading (a Promise return type) or explicit relations in your queries to avoid N+1 performance issues
- QueryBuilder with leftJoinAndSelect is the most predictable approach for complex, multi-relation queries
Category 5: Microservices and Advanced (Q25-Q30)
The final stretch of a senior NestJS interview usually moves past CRUD and into how the framework scales: microservices, messaging patterns, CQRS, testing strategy, and caching. These are the questions that separate someone who has shipped a single API from someone who has run NestJS in a distributed system.
Q25. What microservice transports does NestJS support?
NestJS has a built-in microservices module that abstracts over multiple transport layers, so the same controller patterns work whether the underlying transport is TCP or a message broker.
| Transport | What it's good for |
|---|---|
| TCP | Default, in-process communication; good for services on the same network |
| Redis | Pub/sub messaging using Redis |
| RabbitMQ | Message broker with routing keys, exchanges, and queues |
| Kafka | High-throughput event streaming |
| gRPC | Binary protocol, strongly typed with protocol buffers, lower latency |
| NATS | Lightweight, cloud-native messaging |
// Hybrid app (HTTP + microservice)
const app = await NestFactory.create(AppModule);
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.RABBITMQ,
options: {
urls: [process.env.RABBITMQ_URL],
queue: 'orders_queue',
queueOptions: { durable: true },
},
});
await app.startAllMicroservices();
await app.listen(3000);Q26. What is the difference between @MessagePattern and @EventPattern?
Both decorators mark handlers in a microservice controller, but the communication model behind them is different, and naming that difference precisely is the point of the question.
@MessagePattern handles request-response messages: the caller sends a message and waits for a reply. Use it when the caller actually needs a result back.
@MessagePattern({ cmd: 'get_user' })
async getUser(@Payload() data: { id: string }): Promise<User> {
return this.usersService.findOne(data.id);
}
// Caller
const user = await this.client.send({ cmd: 'get_user' }, { id }).toPromise();@EventPattern handles fire-and-forget events: the caller emits and doesn't wait for a response. Use it for notifications, audit logging, and asynchronous side effects where the caller doesn't need to know the outcome immediately.
@EventPattern('user_created')
async handleUserCreated(@Payload() data: UserCreatedEvent) {
await this.analyticsService.track(data);
}
// Caller
this.client.emit('user_created', { id, email, timestamp });Q27. What is CQRS in the context of NestJS?
CQRS, Command Query Responsibility Segregation, separates write operations (commands) from read operations (queries). The @nestjs/cqrs package provides a CommandBus, QueryBus, and EventBus to implement this pattern without building the plumbing yourself.
// Command
export class CreateOrderCommand {
constructor(public readonly dto: CreateOrderDto) {}
}
// Command Handler
@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
constructor(private readonly ordersRepository: OrdersRepository) {}
async execute(command: CreateOrderCommand) {
return this.ordersRepository.create(command.dto);
}
}
// Controller dispatches commands and queries
@Post()
async create(@Body() dto: CreateOrderDto) {
return this.commandBus.execute(new CreateOrderCommand(dto));
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.queryBus.execute(new GetOrderQuery(id));
}CQRS is most valuable in complex domains where reads and writes have different scaling requirements, or where you're using event sourcing. It's worth being honest in an interview that it adds real complexity, and naming the cases where it pays for itself is more convincing than presenting it as a default choice.
Q28. How do you test a NestJS service with Jest?
NestJS uses Jest by default, and the Test module from @nestjs/testing creates a test DI container where you can swap real providers for mocks without touching the class under test.
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<Repository<User>>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: {
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
},
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repo = module.get(getRepositoryToken(User));
});
it('should return a user by id', async () => {
const mockUser = { id: '123', email: 'test@example.com' };
repo.findOne.mockResolvedValue(mockUser as User);
const result = await service.findOne('123');
expect(result).toEqual(mockUser);
expect(repo.findOne).toHaveBeenCalledWith({ where: { id: '123' } });
});
});Q29. How do you write an end-to-end test in NestJS?
End-to-end tests use Supertest to make real HTTP requests against the full NestJS application. They live in the test/ folder and exercise the actual request pipeline, not mocked pieces of it.
describe('UsersController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(UsersService)
.useValue({
findOne: jest.fn().mockResolvedValue({ id: '1', email: 'a@b.com' }),
})
.compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/users/:id (GET)', () => {
return request(app.getHttpServer())
.get('/users/1')
.set('Authorization', 'Bearer test-token')
.expect(200)
.expect({ id: '1', email: 'a@b.com' });
});
});Override external dependencies, like the database or an email service, in your E2E tests so they run fast and don't require any external infrastructure to pass in CI.
Q30. How do you implement caching in NestJS?
NestJS has a built-in caching module, @nestjs/cache-manager, that works with in-memory storage by default and Redis for production. You can apply it broadly with an interceptor or precisely with manual cache control inside a service.
// app.module.ts
@Module({
imports: [
CacheModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (config: ConfigService) => ({
store: redisStore,
host: config.get('REDIS_HOST'),
port: config.get('REDIS_PORT'),
ttl: 300, // seconds
}),
isGlobal: true,
}),
],
})
export class AppModule {}
// Auto-cache an entire route response
@UseInterceptors(CacheInterceptor)
@Get('products')
findAll() {
return this.productsService.findAll();
}
// Manual cache control in a service
@Injectable()
export class ProductsService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
async findOne(id: string): Promise<Product> {
const cached = await this.cacheManager.get<Product>(`product:${id}`);
if (cached) return cached;
const product = await this.productsRepository.findById(id);
await this.cacheManager.set(`product:${id}`, product, 300);
return product;
}
async invalidate(id: string) {
await this.cacheManager.del(`product:${id}`);
}
}Use CacheInterceptor for simple GET route caching, and manual cache control when you need finer granularity: different TTLs per entity, cache invalidation on mutations, or composite cache keys. If you want a deeper look at the async patterns underneath caching and other I/O-heavy NestJS code, these async/await mistakes that slow down JavaScript cover the traps that show up just as often in service layers as in front-end code.
Quick Reference: All 30 Questions at a Glance
Use this as a final scan the night before an interview. If any row makes you pause, jump back up to that question's full answer.
| # | Question | Core concept |
|---|---|---|
| 1 | What is NestJS vs. plain Express? | Framework overview |
| 2 | What does a Module contain? | @Module, imports/exports |
| 3 | Controller vs. Provider | Routing vs. business logic |
| 4 | How does Dependency Injection work? | IoC container, constructor injection |
| 5 | Types of Providers | Class, Value, Factory, Existing |
| 6 | What are Decorators? | Metadata, @Injectable, custom decorators |
| 7 | Request lifecycle execution order | Middleware > Guards > Interceptors > Pipes > Handler |
| 8 | Circular dependency resolution | Refactor first, forwardRef as last resort |
| 9 | What is Middleware? | Pre-pipeline, req/res/next |
| 10 | How do Guards work? | CanActivate, auth/authz |
| 11 | What are Pipes? | Validation and transformation |
| 12 | What are Interceptors? | Wrap execution, RxJS Observable |
| 13 | What are Exception Filters? | Catch and format errors |
| 14 | Guard vs. Middleware vs. Interceptor | When to use each |
| 15 | Custom Pipe implementation | PipeTransform interface |
| 16 | @Param vs. @Body vs. @Query vs. @Headers | Request extraction decorators |
| 17 | JWT authentication with Passport | JwtModule, JwtStrategy, AuthGuard |
| 18 | Role-based access control | SetMetadata, Reflector, RolesGuard |
| 19 | Environment config with ConfigModule | ConfigModule.forRoot, Joi validation |
| 20 | API security best practices | Helmet, throttler, ValidationPipe |
| 21 | TypeORM integration | TypeOrmModule, Repository, entities |
| 22 | Mongoose integration | MongooseModule, Schema, InjectModel |
| 23 | Database transactions | QueryRunner, DataSource.transaction |
| 24 | TypeORM relations | OneToMany, ManyToMany, JoinTable |
| 25 | Microservice transports | TCP, Redis, RabbitMQ, Kafka, gRPC |
| 26 | @MessagePattern vs. @EventPattern | Request-response vs. fire-and-forget |
| 27 | CQRS pattern in NestJS | CommandBus, QueryBus, EventBus |
| 28 | Unit testing with Jest | Test module, mocked providers |
| 29 | E2E testing with Supertest | Full app, HTTP assertions |
| 30 | Caching with cache-manager and Redis | CacheInterceptor, manual cache control |
Frequently Asked Questions
What is NestJS, and what kind of projects is it best suited for?
NestJS is a TypeScript-first Node.js framework that adds modules, dependency injection, and decorators on top of Express or Fastify. It's best suited for backend projects where structure and long-term maintainability matter more than getting a prototype out the door in an afternoon: REST APIs, GraphQL backends, microservices, and enterprise systems with multiple teams working on the same codebase.
If you're building a quick script or a tiny single-purpose service, plain Express or Fastify is often less overhead. NestJS earns its structure on projects that are going to grow, get tested, and outlive their first author.
How does NestJS compare to using Express directly?
NestJS is built on top of Express (or Fastify) rather than replacing it, so you get the same underlying HTTP handling with an opinionated architecture layered on top: modules for organizing features, a dependency injection container for managing instances, and decorators for wiring routes and validation declaratively.
| Plain Express | NestJS | |
|---|---|---|
| Structure | You decide; no enforced convention | Module-based structure enforced by the framework |
| Dependency management | Manual instantiation and wiring | Built-in DI container resolves and injects automatically |
| TypeScript | Supported, but bolted on rather than foundational | First-class, baked into the framework's design |
| Testing | You assemble your own mocking setup | @nestjs/testing ships a test DI container out of the box |
Is NestJS a good fit for building microservices?
Yes. NestJS ships a dedicated microservices module that abstracts over transports like TCP, Redis, RabbitMQ, Kafka, gRPC, and NATS, and it lets a single application run as both an HTTP API and a microservice through hybrid application setup (covered in Q25 of this guide).
The same controller and provider patterns you use for REST APIs carry over to @MessagePattern and @EventPattern handlers, which keeps the learning curve relatively shallow once you understand the framework's core architecture.
How do I add request validation to a NestJS endpoint?
Define a DTO class with class-validator decorators, then apply ValidationPipe either globally or on the specific route. NestJS runs the pipe before the handler executes, so invalid requests never reach your business logic.
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
}
@Post()
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}Setting whitelist and forbidNonWhitelisted together gives you the strictest behavior: unknown fields are stripped, and a request that includes them is rejected outright rather than silently accepted.
What goes wrong if I put business logic directly in a controller instead of a service?
It still runs, NestJS doesn't enforce the separation at compile time, but you lose most of the benefits the framework is designed to give you. Logic embedded in a controller is harder to unit test (you'd need to spin up the HTTP layer to exercise it), harder to reuse from another controller or a microservice handler, and harder to mock in isolation.
This is also a common interview trap: an interviewer may ask you to review a code sample with logic in the controller and watch whether you flag it. Naming the rule (controllers route, services contain logic) and explaining why it matters for testability is the answer they're listening for.
Related Articles
5 async/await Mistakes That Slow Your JavaScript Code
Sequential awaits, await in forEach, missing Promise.all: these 5 async/await mistakes silently slow your JavaScript. Here's how to spot and fix each one.
npm Scripts You're Probably Not Using (But Should Be)
pre/post hooks, cross-env, npm-run-all, argument passing, and built-in variables: the npm script patterns developers Google one at a time, in one place.
Drizzle ORM Migrations: A Practical drizzle-kit Guide
Learn the full Drizzle ORM migration workflow: push vs migrate, drizzle-kit setup, Turso/libSQL config, team conflicts, and production best practices.