NestJS is a common choice for structured Node.js backends, but the useful interview question is not whether it is universally better than Express. It is whether its dependency injection, modules, lifecycle, and transport abstractions fit the system being built.
This guide covers 27 NestJS interview questions from module structure to guards, interceptors, database integration, and testing. It includes the NestJS 12 changes relevant in 2026.
Table of Contents
- NestJS Fundamentals Questions
- Module Architecture Questions
- Controller Questions
- Dependency Injection Questions
- Validation and Pipes Questions
- Guards and Authorization Questions
- Interceptors and Filters Questions
- Database Integration Questions
- Testing Questions
NestJS Fundamentals Questions
These foundational questions test your understanding of why NestJS exists and when to use it.
What is NestJS and why would you use it over Express?
NestJS is a structured Node.js framework with modules, dependency injection, decorators, and abstractions for HTTP, microservices, GraphQL, and WebSockets. TypeScript is a first-class path, but the value comes from consistent application boundaries and lifecycle hooks rather than from decorators alone.
Express is a smaller HTTP framework and leaves application structure, dependency injection, validation, and many cross-cutting concerns to the team. Nest provides more conventions and integration at the cost of framework concepts, startup work, and abstraction. Either can support a serious system.
For HTTP applications, Express is the default platform adapter and Fastify is an alternative. Adapter-specific request, response, plugin, and middleware APIs can reduce portability, so use Nest abstractions where switching adapters is a real requirement.
When would you NOT use NestJS?
For a tiny endpoint, edge runtime, or function with a strict cold-start budget, a smaller framework may be a better fit. Measure the packaged application rather than assuming NestJS is unsuitable for every serverless deployment. A team that does not benefit from DI and module conventions may also pay complexity without enough return.
The key considerations are domain complexity, deployment model, transport needs, performance budget, and whether the team will consistently use the conventions. Team size alone is not a decision rule.
What is the request lifecycle in NestJS?
Understanding execution order is crucial for debugging and designing middleware correctly. When a request arrives, NestJS processes it through several layers in a specific order.
Middleware runs first, then guards, inbound interceptors, pipes, and the handler. On success, interceptors resolve in reverse order. An uncaught exception skips the remaining lifecycle and enters the matching exception filter; route filters run before controller and global filters. Middleware exceptions can only reach global filters because route selection has not happened yet.
Incoming Request
│
▼
Middleware
│
▼
Guards ──────────▶ (return false = 403)
│
▼
Interceptors (before)
│
▼
Pipes ───────────▶ (validation fails = 400)
│
▼
Handler (Controller method)
│
▼
Interceptors (after, reverse order)
│
▼
Response
Uncaught error ──▶ Exception Filters (route → controller → global)
What changed in NestJS 12?
NestJS 12 moves core packages to ESM while still allowing modern CommonJS applications to consume them through Node's require(esm) support. Running an application requires Node.js 20.19+ or 22.12+ on the Node 22 line, while the CLI schematics have a higher supported-runtime floor; the current active LTS is the simplest choice.
Version 12 adds Standard Schema validation and serialization alongside the existing class-validator workflow, makes Vitest the default for newly generated ESM projects, adds the official @nestjs/observe SDK, and shifts CLI bundling toward Rspack. New code should also review GraphQL, NATS v3, configuration validation, and lifecycle-hook migration notes before upgrading. Use nest upgrade --dry-run and keep all @nestjs/* packages on the same compatible major.
Module Architecture Questions
Modules are the fundamental organizational unit in NestJS.
What are NestJS modules and how do they work?
Modules are classes decorated with @Module() that organize application structure. Each module encapsulates a feature—users, auth, orders—and declares its controllers, providers, imports, and exports. The root AppModule imports all feature modules, and NestJS builds the dependency graph at startup.
This modular architecture creates clear boundaries between features and makes it easy to understand dependencies at a glance.
// users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])], // Import dependencies
controllers: [UsersController], // Handle HTTP requests
providers: [UsersService], // Business logic & DI
exports: [UsersService] // Make available to other modules
})
export class UsersModule {}How do module imports and exports work?
Imports bring in functionality from other modules, allowing you to use their exported providers. Exports make providers available to modules that import this module. Providers are scoped to their module by default—they're not globally available unless explicitly exported.
This encapsulation prevents tight coupling between modules and makes dependencies explicit.
// app.module.ts (root module)
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
// ... config
}),
UsersModule,
AuthModule,
],
})
export class AppModule {}Key points:
- Each module encapsulates a feature (users, auth, orders)
importsbring in other modules' exported providersexportsmake providers available to importing modules- Providers are module-scoped by default
Controller Questions
Controllers handle incoming HTTP requests and return responses.
How do controllers work in NestJS?
Controllers are classes decorated with @Controller() that handle incoming HTTP requests. They use decorators like @Get(), @Post(), @Put(), @Delete() to define route handlers. Parameter decorators like @Body(), @Param(), and @Query() extract data from requests.
Controllers should be thin—they receive requests, delegate to services for business logic, and return responses. This separation keeps code testable and maintainable.
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
ParseIntPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users') // Route prefix: /users
export class UsersController {
// Dependency injection via constructor
constructor(private readonly usersService: UsersService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
findAll(@Query('role') role?: string) {
return this.usersService.findAll(role);
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
@Put(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number) {
return this.usersService.remove(id);
}
}How does NestJS compare to Express for route handling?
NestJS provides built-in pipes for parsing and validation, reducing boilerplate and improving type safety. Express requires manual parsing and validation at each route.
// Express - manual parsing
router.get('/users/:id', (req, res) => {
const id = parseInt(req.params.id); // Manual parsing
if (isNaN(id)) {
return res.status(400).json({ error: 'Invalid ID' });
}
// ...
});
// NestJS - ParseIntPipe handles parsing and validation
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id is already a number, invalid input throws BadRequestException
}Dependency Injection Questions
Dependency injection is central to NestJS architecture.
What is dependency injection and how does it work in NestJS?
Dependency injection (DI) is a design pattern where classes receive their dependencies from external sources rather than creating them. In NestJS, the IoC (Inversion of Control) container manages service instantiation and injection automatically.
You mark classes with @Injectable() to tell NestJS they can be managed by the container, register them as providers in modules, and inject them through constructor parameters. This enables loose coupling, easier testing with mock dependencies, and better code organization.
// 1. Mark class as injectable
@Injectable()
export class CatsService {
constructor(private readonly logger: LoggerService) {}
}
// 2. Register as provider in module
@Module({
providers: [CatsService, LoggerService],
})
export class CatsModule {}
// 3. Inject via constructor
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
}How do you create services in NestJS?
Providers decorated with @Injectable() can contain application or domain orchestration and inject ports, repositories, or other providers. Keep controllers focused on transport concerns, but do not turn one large service class into the only place where all business logic lives.
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable() // Marks class for DI container
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}
async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
async remove(id: number): Promise<void> {
const result = await this.usersRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`User #${id} not found`);
}
}
}What are provider scopes in NestJS?
NestJS supports three provider scopes that determine how instances are created and shared. Understanding scopes is important for managing state and performance.
The default singleton scope creates one instance shared across the application context. Request scope creates an instance per request context and bubbles up to consumers that depend on it. A transient provider creates a dedicated instance for each consumer that injects it.
import { Injectable, Scope } from '@nestjs/common';
// DEFAULT - Singleton (one instance shared across app)
@Injectable()
export class SingletonService {}
// REQUEST - New instance per request
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {}
// TRANSIENT - New instance each time injected
@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {}When to use each:
- Singleton (default): Most services—stateless business logic
- Request: When you need request-specific data (current user, tenant)
- Transient: When each consumer needs its own instance (rare)
Keep singleton providers free of request-specific mutable state: Node.js can interleave many asynchronous requests even though JavaScript execution is single-threaded. Request scope adds allocation and can make an entire dependency subtree request-scoped, so prefer explicit context propagation or durable providers when appropriate.
How do you create custom providers?
Custom providers give you fine-grained control over how dependencies are created and resolved. You can provide values directly, use factory functions, or swap implementations based on environment.
export const API_KEY = Symbol('API_KEY');
@Module({
providers: [
// Standard provider
UsersService,
// Value provider
{
provide: API_KEY,
useValue: process.env.API_KEY,
},
// Factory provider
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
return createConnection(configService.get('database'));
},
inject: [ConfigService],
},
// Class provider with different implementation
{
provide: LoggerService,
useClass: process.env.NODE_ENV === 'test'
? MockLoggerService
: ProductionLoggerService,
},
],
})
export class AppModule {}
// Inject custom provider
@Injectable()
export class SomeService {
constructor(@Inject(API_KEY) private apiKey: string) {}
}Prefer exported symbols or classes over repeated string tokens to avoid collisions. Validate required configuration before constructing a value provider rather than allowing undefined to enter the dependency graph.
Validation and Pipes Questions
Validation is critical for API security and data integrity.
How do you handle validation in NestJS?
For class-based DTOs, NestJS supports class-validator decorators with ValidationPipe. Use concrete classes and runtime imports because TypeScript interfaces, generics, and type-only imports do not provide the metadata needed at runtime. NestJS 12 also supports Standard Schema libraries such as Zod and Valibot through StandardSchemaValidationPipe.
Choose the class-based or schema-first approach consistently. Validation protects an API boundary, but authorization and database constraints still enforce different invariants.
// dto/create-user.dto.ts
import {
IsEmail,
IsNotEmpty,
IsString,
MinLength,
IsOptional,
IsEnum,
} from 'class-validator';
import { Transform } from 'class-transformer';
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
}
export class CreateUserDto {
@IsNotEmpty()
@IsString()
@Transform(({ value }) => value.trim())
name: string;
@IsEmail()
@Transform(({ value }) => value.toLowerCase())
email: string;
@IsString()
@MinLength(8, { message: 'Password must be at least 8 characters' })
password: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole = UserRole.USER;
}
// dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
// All fields optional, same validation rules
export class UpdateUserDto extends PartialType(CreateUserDto) {}How do you configure the global ValidationPipe?
For class-based DTOs, ValidationPipe is often configured globally. whitelist strips fields without validation metadata, forbidNonWhitelisted rejects them, and transform creates DTO instances and can convert primitives. Implicit conversion can hide surprising coercions, so prefer explicit parse pipes or schema coercion for sensitive inputs.
// main.ts
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Strip non-whitelisted properties
forbidNonWhitelisted: true, // Throw error on extra properties
transform: true, // Auto-transform payloads to DTO types
transformOptions: {
enableImplicitConversion: true,
},
}));
await app.listen(3000);
}How do you create custom pipes?
Custom pipes implement the PipeTransform interface and can transform or validate input. They're useful for custom parsing logic that isn't covered by built-in pipes.
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
transform(value: string): Date {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new BadRequestException(`Invalid date: ${value}`);
}
return date;
}
}
// Usage
@Get()
findByDate(@Query('date', ParseDatePipe) date: Date) {
return this.service.findByDate(date);
}Guards and Authorization Questions
Guards handle authorization and access control.
What are guards and how do they work?
Guards implement CanActivate and decide whether execution may continue. Returning false produces a 403 response; an authentication guard should usually throw UnauthorizedException for missing or invalid credentials so clients receive 401. ExecutionContext exposes the handler, controller, and transport-specific context.
Guards run after middleware but before interceptors and pipes, making them ideal for authentication and authorization checks.
// auth/guards/jwt-auth.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractToken(request);
if (!token) throw new UnauthorizedException();
try {
const payload = await this.jwtService.verifyAsync(token);
request.user = payload;
return true;
} catch {
throw new UnauthorizedException();
}
}
private extractToken(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}Configure JwtService with an explicit algorithm, issuer, audience, key source, and rotation strategy. This short guard demonstrates lifecycle placement, not a complete token-security policy.
How do you implement role-based authorization?
Role-based authorization combines a guard with a custom decorator. The decorator sets metadata on the route, and the guard reads that metadata to check if the user has the required role.
// auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride(Roles, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true; // No roles required
}
const { user } = context.switchToHttp().getRequest();
return Boolean(user && requiredRoles.includes(user.role));
}
}
// auth/decorators/roles.decorator.ts (NestJS 12 preferred form)
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();How do you apply guards to controllers?
Guards can be applied at the method level, controller level, or globally. Controller-level guards protect all routes in that controller. Global guards protect the entire application.
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard) // Apply to all routes in controller
export class AdminController {
@Get('dashboard')
@Roles(['admin']) // Only admins
getDashboard() {
return { message: 'Admin dashboard' };
}
@Get('stats')
@Roles(['admin', 'moderator']) // Admins and moderators
getStats() {
return { message: 'Stats' };
}
}
// Or apply globally in app.module.ts
@Module({
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}Interceptors and Filters Questions
Interceptors and filters handle cross-cutting concerns.
What are interceptors and when do you use them?
Interceptors implement NestInterceptor and wrap the handler execution. They can add logic before and after the handler runs, making them ideal for logging, caching, response transformation, and performance monitoring.
Interceptors use RxJS observables, allowing powerful stream manipulation of responses.
// interceptors/logging.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
const now = Date.now();
return next.handle().pipe(
tap(() => {
console.log(`${method} ${url} - ${Date.now() - now}ms`);
}),
);
}
}How do you transform responses with interceptors?
A transform interceptor wraps all responses in a consistent format. This is useful for standardizing your API responses across all endpoints.
Apply envelopes deliberately: raw streams, file downloads, server-sent events, GraphQL responses, and handlers using the platform response object may need to bypass a generic transform interceptor. Response schemas in NestJS 12 can also be enforced with StandardSchemaSerializerInterceptor.
// interceptors/transform.interceptor.ts
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}What are exception filters and how do you use them?
Exception filters catch exceptions thrown during request handling and format error responses. They implement ExceptionFilter and use the @Catch() decorator to specify which exceptions to handle.
// filters/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
response.status(status).json({
success: false,
statusCode: status,
message,
path: request.url,
timestamp: new Date().toISOString(),
});
}
}
// Apply globally
app.useGlobalFilters(new AllExceptionsFilter());Do not expose arbitrary exception messages or stacks for unknown errors. Log the original error with correlation context and return a stable 500 payload. Register dependency-injected global filters with the APP_FILTER provider rather than constructing them manually in main.ts.
How do you create custom exceptions?
Custom exceptions extend HttpException and provide meaningful error messages for specific scenarios. This makes error handling more expressive and consistent.
import { HttpException, HttpStatus } from '@nestjs/common';
export class UserNotFoundException extends HttpException {
constructor(userId: number) {
super(`User with ID ${userId} not found`, HttpStatus.NOT_FOUND);
}
}
export class InsufficientPermissionsException extends HttpException {
constructor() {
super('Insufficient permissions', HttpStatus.FORBIDDEN);
}
}
// Usage
throw new UserNotFoundException(userId);Database Integration Questions
Database integration is essential for most NestJS applications.
How do you integrate TypeORM with NestJS?
The @nestjs/typeorm package integrates TypeORM repositories with Nest dependency injection. Define entities, register them with forFeature(), and inject repositories, while keeping migrations and production schema synchronization as explicit deployment concerns.
// entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column({ select: false }) // Exclude from queries by default
password: string;
@Column({ default: 'user' })
role: string;
@CreateDateColumn()
createdAt: Date;
}
// users.module.ts
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}How do you integrate Prisma with NestJS?
Prisma provides generated TypeScript queries and can be exposed through a Nest provider. Prisma ORM 7 requires a database driver adapter and a generated client output path; new PrismaClient() without an adapter now fails.
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from './generated/prisma/client.js';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() {
super({
adapter: new PrismaPg({
connectionString: process.env.DATABASE_URL as string,
}),
});
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
// users.service.ts
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async findAll() {
return this.prisma.user.findMany();
}
async findOne(id: number) {
return this.prisma.user.findUnique({ where: { id } });
}
}How do you handle configuration in NestJS?
The @nestjs/config package loads and exposes configuration. Validate environment input at startup and fail fast. In NestJS 12, validationSchema accepts Standard Schema-compatible libraries such as Zod, Valibot, ArkType, or Joi 18+.
// Using @nestjs/config
import { ConfigModule, ConfigService } from '@nestjs/config';
import { z } from 'zod';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV}`,
validationSchema: z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
DATABASE_URL: z.string().min(1),
}),
}),
],
})
export class AppModule {}
// Inject and use
@Injectable()
export class SomeService {
constructor(private configService: ConfigService) {}
getDatabaseUrl() {
return this.configService.get<string>('DATABASE_URL');
}
}Testing Questions
NestJS provides testing utilities that work with different test runners; new NestJS 12 ESM projects use Vitest by default.
How do you unit test services in NestJS?
The Test builder creates a Nest testing module with dependency injection. Mock only the boundary needed by the test and keep separate integration tests for the real database adapter and mappings.
// users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { vi } from 'vitest';
import { UsersService } from './users.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
describe('UsersService', () => {
let service: UsersService;
let mockRepository: any;
beforeEach(async () => {
mockRepository = {
find: vi.fn(),
findOne: vi.fn(),
create: vi.fn(),
save: vi.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should find all users', async () => {
const users = [{ id: 1, name: 'John' }];
mockRepository.find.mockResolvedValue(users);
expect(await service.findAll()).toEqual(users);
expect(mockRepository.find).toHaveBeenCalled();
});
it('should throw NotFoundException for missing user', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
});
});How do you write e2e tests for NestJS controllers?
End-to-end tests create an application instance and make HTTP requests with Supertest. They exercise only what the test application actually registers: global pipes, filters, middleware, and prefixes configured solely in main.ts must be applied through shared bootstrap code or repeated in the test setup.
// users.controller.spec.ts (e2e style)
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
describe('UsersController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
it('/users (GET)', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
});
});
afterAll(async () => {
await app.close();
});
});Quick Reference
| Concept | Decorator/Class | Purpose |
|---|---|---|
| Module | @Module() | Organize code, define boundaries |
| Controller | @Controller() | Handle HTTP requests |
| Service | @Injectable() | Business logic, DI-managed |
| Guard | CanActivate | Authorization, access control |
| Pipe | PipeTransform | Validation, transformation |
| Interceptor | NestInterceptor | Wrap handler, modify request/response |
| Filter | ExceptionFilter | Handle exceptions |
| Middleware | NestMiddleware | Pre-route logic (like Express) |
NestJS vs Express Comparison
| Aspect | Express | NestJS |
|---|---|---|
| Structure | You decide | Modules, controllers, services |
| DI | Manual or external lib | Built-in IoC container |
| TypeScript | Optional | First-class support |
| Validation | User-selected middleware or schemas | ValidationPipe or Standard Schema pipe |
| Testing | Manual setup | Test module with DI mocking |
| Learning curve | Lower | Higher |
| Best fit | Minimal HTTP layer and custom architecture | Teams that benefit from Nest modules, DI, and transports |
Related Articles
- Complete Node.js Backend Developer Interview Guide - comprehensive preparation guide for backend interviews
- Express.js Middleware Interview Guide - Compare Express patterns to NestJS
- TypeScript Generics Interview Guide - TypeScript patterns used heavily in NestJS
- Authentication & JWT Interview Guide - Auth patterns for NestJS apps
Official References
- NestJS 12 migration guide
- NestJS request lifecycle
- NestJS injection scopes
- NestJS validation
- NestJS configuration
- NestJS testing
- Prisma ORM 7 with NestJS
Frequently Asked Questions
What is NestJS and why use it over Express?
NestJS is a structured Node.js framework with modules, dependency injection, decorators, testing utilities, and adapters for Express or Fastify. Express is a smaller HTTP framework, while Nest adds application architecture and cross-transport abstractions. Choose from domain complexity, team conventions, runtime constraints, and ecosystem needs—not an arbitrary team-size threshold.
What is dependency injection in NestJS?
Dependency injection (DI) is a design pattern where classes receive their dependencies from external sources rather than creating them. In NestJS, the IoC container manages service instantiation and injection. You mark classes with @Injectable(), register them as providers in modules, and inject them through constructor parameters. DI enables loose coupling, easier testing with mock dependencies, and better code organization.
What is the difference between Guards, Pipes, and Interceptors?
Guards decide whether execution may continue, pipes validate or transform handler arguments, and interceptors wrap handler execution. The usual inbound order is middleware, guards, interceptors, pipes, then the handler; interceptors resolve in reverse order on the return path. Exception filters run only for uncaught exceptions and resolve from route to controller to global scope.
How do you structure a NestJS application?
NestJS uses a modular architecture. The root AppModule imports feature modules (UsersModule, AuthModule). Each feature module contains controllers (handling HTTP requests), services (business logic), and entities/DTOs (data structures). Modules can export providers for use by other modules. Follow the single responsibility principle - each module handles one domain area. Use shared modules for common functionality like logging or utilities.
How do you handle validation in NestJS?
For class-based DTOs, use class-validator and ValidationPipe; concrete classes and runtime imports are required because interfaces and type-only imports are erased. NestJS 12 also supports Standard Schema libraries such as Zod or Valibot through StandardSchemaValidationPipe. Configure whitelist or rejection of unknown fields deliberately and avoid exposing sensitive validation details.
What are NestJS modules and how do imports/exports work?
Modules are classes decorated with @Module() that organize application structure. Each module declares its controllers, providers, imports, and exports. Imports bring in functionality from other modules. Exports make providers available to modules that import this module. Providers are scoped to their module by default. The root AppModule imports all feature modules, and NestJS builds the dependency graph at startup.
