The 60 NestJS questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
NestJS is a framework for building server-side Node.js applications, created by Kamil Mysliwiec and first released in 2017. It's written in TypeScript, runs on top of Express by default (Fastify is an option), and borrows Angular's ideas: decorators, modules, and dependency injection. Where plain Express leaves architecture up to you, Nest hands you a consistent structure so large teams organize code the same way. In interviews, NestJS questions probe the dependency injection container, the request pipeline (guards, interceptors, pipes, exception filters), and how modules scope providers, not memorized decorator names. This page collects the 60 questions that come up most, each with a direct answer and runnable code. If you're building fundamentals, the official NestJS documentation is the canonical path; increasingly the first backend round runs as a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: NestJS Crash Course
Video: NestJS Crash Course (Traversy Media, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your NestJS certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
NestJS is a TypeScript-first framework for building server-side Node.js applications. It runs on top of Express by default and adds an opinionated structure: modules, controllers, providers, and a built-in dependency injection container.
The problem it solves is architecture drift. Plain Express leaves file layout and wiring up to each team, so large codebases diverge. Nest hands you conventions borrowed from Angular, so a big team organizes code the same way and features stay testable.
Key point: A one-line definition plus the 'why it exists' (structure over unopinionated Express) beats listing decorators. Interviewers open with this to hear how you frame an answer.
Nest is built on top of Express, so it isn't a replacement so much as a structured layer over it. Express gives you routing and middleware and nothing else; you decide the folder layout, how services are wired, and how errors are handled.
Nest adds modules, dependency injection, decorators, and a defined request lifecycle. You trade a heavier learning curve and more files for consistency that pays off as the team and codebase grow.
| Express | NestJS | |
|---|---|---|
| Structure | Unopinionated | Opinionated (modules, DI) |
| Language | JavaScript (TS optional) | TypeScript-first |
| Dependency injection | Manual | Built-in container |
| Best for | Small services, full control | Large team apps needing consistency |
Key point: Say 'Nest runs on Express under the hood' early. It shows you understand Nest isn't magic, just structure plus DI on a familiar base.
A module is a class decorated with @Module() that groups related code: controllers, providers, and the modules it imports and exports. Every Nest app has at least a root AppModule, and features get their own modules.
Modules are how Nest scopes dependency injection. A provider is available inside its module by default, and only becomes available to other modules if the owning module exports it and the other module imports it.
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // share UsersService with importing modules
})
export class UsersModule {}A controller is a class decorated with @Controller() whose methods handle incoming requests. The decorator's argument sets a route prefix, and method decorators like @Get() and @Post() map HTTP verbs and sub-paths to handlers.
Controllers should stay thin: read the request, call a service, return the result. Business logic belongs in providers, not in the controller.
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string) {
return { id };
}
@Post()
create(@Body() body: any) {
return body;
}
}A provider is any class Nest can inject as a dependency, most often a service holding business logic. Marking it with @Injectable() tells Nest's container it can manage and inject that class.
Once a provider is registered in a module's providers array, Nest creates it and hands it to any controller or provider that asks for it in a constructor. That is dependency injection in Nest.
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private users = [{ id: '1', name: 'Asha' }];
findOne(id: string) {
return this.users.find((u) => u.id === id);
}
}Key point: 'What does @Injectable() actually do?' is a common probe. The right answer: it registers the class with the DI container so Nest can construct and inject it.
You declare a dependency by typing a constructor parameter, and Nest's DI container supplies the instance. You never call new on a service; the container resolves the whole graph and injects singletons where they're needed.
This inverts control: a class states what it needs, and the framework provides it. That makes classes easier to test because you can pass a mock in place of the real dependency.
@Controller('users')
export class UsersController {
// Nest injects the singleton UsersService here
constructor(private readonly usersService: UsersService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
}Key point: This is the single most important NestJS concept. If you can explain constructor injection and why it helps testing, you clear the bar for a fresher round.
Watch a deeper explanation
Video: NestJS Tutorial #10 | Inversion of Control and Dependency Injection (Shameel Uddin, YouTube)
Decorators are functions prefixed with @ that attach metadata to classes, methods, or parameters. Nest reads that metadata at startup to wire routes, injection, validation, and the request lifecycle.
That's why @Controller(), @Get(), @Injectable(), @Body(), and @Param() are everywhere: they describe intent declaratively, and Nest turns the metadata into behavior. Decorators come from TypeScript, and Nest builds its whole model on them.
The Nest CLI is a command-line tool that scaffolds and runs projects. nest new creates an app, and nest generate (or nest g) creates modules, controllers, services, and more with the right files, decorators, and test stubs already wired.
Teams use it to keep structure consistent: generating a resource produces the module, controller, service, and DTOs in one command, so everyone's files look the same.
nest new my-app
nest generate module users
nest generate controller users
nest generate service users
# or scaffold a full CRUD resource at once:
nest generate resource usersmain.ts is the entry point. It calls NestFactory.create() with the root AppModule to build the application instance, applies any global setup (pipes, prefixes, CORS), and then calls listen() to start the HTTP server.
During create(), Nest resolves the whole module graph and instantiates every singleton provider once, which is why a wiring mistake usually fails loudly at startup rather than at request time.
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();A DTO (data transfer object) is a class that defines the expected shape of data moving into or out of your app, usually a request body. It gives you a typed contract instead of untyped any.
Paired with class-validator decorators and the ValidationPipe, the DTO becomes the validation layer: bad requests get rejected before they reach your service, and the service works with a known shape.
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
email: string;
}Key point: Interviewers like to see DTO plus class-validator plus ValidationPipe named together. That trio is how validation actually works in a Nest app.
Nest gives you parameter decorators: @Body() for the request body, @Param() for route parameters, @Query() for the query string, and @Headers() for headers. You pull exactly what you need instead of touching the raw request object.
You can grab a single value (@Param('id')) or the whole object (@Body()). Pairing @Body() with a DTO type is what enables validation.
@Get()
search(@Query('term') term: string) {
return this.usersService.search(term);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}The ValidationPipe runs class-validator rules against incoming DTOs and rejects requests that fail, returning a 400 with the reasons. It reads the decorators on your DTO classes to know what valid means.
You usually enable it globally in main.ts. Options like whitelist strip unknown properties, and transform turns the plain payload into an instance of the DTO class.
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // drop properties not in the DTO
transform: true, // return a real DTO instance
}),
);Throw one of Nest's built-in HTTP exceptions, like NotFoundException or BadRequestException, and Nest's default exception filter turns it into the right status code and JSON body. You don't set res.status by hand.
For a custom status, throw new HttpException(message, status). Unhandled errors become a 500, so throwing the specific exception is how you control the response.
import { NotFoundException } from '@nestjs/common';
@Get(':id')
findOne(@Param('id') id: string) {
const user = this.usersService.findOne(id);
if (!user) {
throw new NotFoundException(`User ${id} not found`);
}
return user;
}Middleware is a function that runs before the route handler, with access to the request and response objects. It's the same concept as Express middleware: logging, request enrichment, raw header checks.
You register it in a module by implementing NestModule and configuring routes in the configure() method. Middleware runs first in the lifecycle, before guards.
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}
}A guard is a class that decides whether a request is allowed to reach the handler. Its canActivate() method returns true to continue or false (or throws) to block, which is how Nest models authentication and authorization.
Guards run after middleware but before interceptors and pipes, and they have access to the execution context, so they can read the request, roles, and metadata set by decorators.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
return Boolean(req.headers['authorization']);
}
}Key point: Guards answer 'may this request proceed?'. If you conflate guards with validation (that's pipes) or response shaping (that's interceptors), the interviewer will notice.
Pipes do two jobs: transformation and validation. They run just before the handler on the arguments bound by decorators. ParseIntPipe transforms a string param to a number; ValidationPipe validates a DTO.
You apply a pipe at the parameter, handler, controller, or global level. If a pipe throws, the handler never runs, which is how bad input gets stopped early.
import { ParseIntPipe } from '@nestjs/common';
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// id is guaranteed to be a number here
return this.usersService.findOne(id);
}A controller handles the HTTP layer: it maps routes to methods, reads the request, and returns a response. A provider (usually a service) holds the business logic and is injected into controllers.
The separation keeps controllers thin and logic reusable and testable. The same service can be used by several controllers, a scheduled job, or a message handler.
| Controller | Provider (service) | |
|---|---|---|
| Decorator | @Controller() | @Injectable() |
| Job | Handle HTTP requests/responses | Business logic, data access |
| Injected into others? | No | Yes, via constructor |
| Should stay | Thin | Where the real work lives |
By default a provider is private to its module. To use it elsewhere, add it to the owning module's exports array, then import that module wherever you need the provider.
This is the most common wiring bug for beginners: injecting a service from another module without exporting it and importing the module, which produces a 'Nest can't resolve dependencies' error at startup.
@Module({
providers: [UsersService],
exports: [UsersService], // 1. export it
})
export class UsersModule {}
@Module({
imports: [UsersModule], // 2. import the module
})
export class OrdersModule {} // now OrdersModule can inject UsersServiceKey point: The classic error 'Nest can't resolve dependencies of X' almost always means a missing export or import. Say that out loud; it shows you've debugged real Nest apps.
A handler can return a Promise, and Nest awaits it before sending the response. Mark the method async and return the awaited value; Nest also accepts an RxJS Observable and resolves it.
You return the data, not the response object. Nest serializes whatever you return to JSON with a 200 (or 201 for POST) unless you throw an exception or set a different status.
@Get(':id')
async findOne(@Param('id') id: string) {
const user = await this.usersService.findOne(id);
return user; // Nest awaits the promise and serializes the result
}The @nestjs/config package provides ConfigModule, which loads .env files and exposes values through an injectable ConfigService. You import ConfigModule.forRoot() once, often with isGlobal: true, then inject ConfigService anywhere.
This keeps environment reads in one typed place instead of scattering process.env across the codebase, and it supports validation of required variables at startup.
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}
@Injectable()
export class MailService {
constructor(private config: ConfigService) {}
key() {
return this.config.get<string>('MAIL_API_KEY');
}
}Nest defaults to 200 for most handlers and 201 for POST. You override with the @HttpCode() decorator on a method, and you signal errors by throwing HTTP exceptions rather than setting a status manually.
This keeps handlers returning plain data. Reaching for the raw response object to set a status is usually a sign you're fighting the framework.
import { HttpCode, Post } from '@nestjs/common';
@Post('login')
@HttpCode(200) // override the default 201 for POST
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}Yes. Nest is platform-agnostic through an adapter. Express is the default, but you can swap in the Fastify adapter for higher throughput and lower overhead, and most of your application code stays the same.
The trade-off: some Express-specific middleware won't work under Fastify, so you check plugin compatibility. For most apps the default Express adapter is fine.
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);Nest maps each HTTP verb to a method decorator: @Get for reads, @Post to create, @Patch or @Put to update, and @Delete to remove. You combine them with a route prefix on the controller and path params on the methods to cover the standard CRUD routes.
Each handler pulls what it needs with @Param, @Body, or @Query and calls the service. Keeping the controller a thin map from route to service method is the pattern the question expects to see.
@Controller('users')
export class UsersController {
constructor(private readonly users: UsersService) {}
@Get() findAll() { return this.users.findAll(); }
@Get(':id') findOne(@Param('id') id: string) { return this.users.findOne(id); }
@Post() create(@Body() dto: CreateUserDto) { return this.users.create(dto); }
@Patch(':id') update(@Param('id') id: string, @Body() dto: UpdateUserDto) { return this.users.update(id, dto); }
@Delete(':id') remove(@Param('id') id: string) { return this.users.remove(id); }
}Watch a deeper explanation
Video: NestJs Course for Beginners - Create a REST API (freeCodeCamp.org, YouTube)
Nest ships with Jest and a Test.createTestingModule() helper that builds a module in memory. You register the class under test, resolve it, and can swap real providers for mocks because everything is injected.
That's the payoff of dependency injection for testing: you never wire a real database into a unit test, you provide a fake in its place.
import { Test } from '@nestjs/testing';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = moduleRef.get(UsersService);
});
it('is defined', () => {
expect(service).toBeDefined();
});
});For candidates with working experience: the request lifecycle in depth, provider scope, and the wiring decisions that separate users from understanders.
For an incoming request the order is: middleware, then guards, then interceptors (pre-controller part), then pipes, then the route handler, then interceptors again (post-controller part), and finally, if anything threw, exception filters.
Knowing this order matters because it decides where each concern belongs: authorization in a guard, input validation in a pipe, response shaping in an interceptor, error formatting in a filter.
NestJS request lifecycle
A common interview trap is putting guards after pipes. Guards run before pipes, so authorization is decided before input is validated.
Key point: Interviewers love asking 'does a guard run before or after a pipe?'. Guards run first. Getting the order right is a strong intermediate signal.
Watch a deeper explanation
Video: Nest.js Full Course for Beginners | Complete All-in-One Tutorial | 3 Hours (Dave Gray, YouTube)
Each owns one concern. A guard decides if a request may proceed. A pipe validates and transforms the incoming arguments. An interceptor wraps the handler to add behavior before and after, like logging or response shaping. An exception filter catches thrown errors and formats the response.
Mixing them up is the most common intermediate mistake. Validation in an interceptor or auth in a pipe technically runs, but it puts the logic in the wrong place and breaks the order guarantees.
| Component | Runs | Job |
|---|---|---|
| Guard | Before pipes | Allow or deny (authorization) |
| Pipe | Before handler | Validate and transform input |
| Interceptor | Around handler | Wrap request/response, logging, caching |
| Exception filter | On thrown error | Catch and format the error response |
Key point: This four-way distinction is the heart of intermediate NestJS. If you can place a new requirement in the right component instantly, you sound experienced.
An interceptor implements NestInterceptor and wraps the handler with an RxJS stream. Code before next.handle() runs before the handler; operators piped onto the returned Observable run after, so you can transform or replace the response.
Typical uses: logging with timing, mapping every response into a consistent envelope, caching, and serialization. The before-and-after nature is what makes them the right tool for cross-cutting response concerns.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map } from 'rxjs/operators';
@Injectable()
export class WrapInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler) {
return next.handle().pipe(map((data) => ({ data })));
}
}You implement ExceptionFilter, decorate it with @Catch() naming the exceptions to handle, and use the arguments host to grab the response and send a shaped error body. Register it globally, on a controller, or on a method.
You write one when the default error shape doesn't fit: to add a request id, log to an external system, or map domain exceptions to a consistent API error format across the whole app.
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const res = host.switchToHttp().getResponse();
const status = exception.getStatus();
res.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}There are three scopes. Singleton (the default) creates one instance shared app-wide. Request-scoped creates a fresh instance per request. Transient creates a new instance for each consumer that injects it.
Stay on singleton almost always; it's the fastest and simplest. Reach for request scope only when a provider must hold per-request state, and know the cost: request scope bubbles up, so anything injecting it also becomes request-scoped and loses the singleton performance.
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
// a new instance is created for every incoming request
}Key point: The follow-up is 'what's the downside of request scope?'. Answer: it propagates up the injection chain and costs you the singleton performance, so use it sparingly.
Beyond the shorthand of listing a class, you can register providers explicitly. useClass swaps one implementation for another, useValue injects a constant or mock, and useFactory computes the provider, optionally injecting other providers to build it.
These power configuration, swapping implementations by environment, and injecting third-party clients. A factory is the usual way to create something like a database client that needs config values.
@Module({
providers: [
{ provide: 'API_KEY', useValue: process.env.API_KEY },
{
provide: PaymentGateway,
useFactory: (config: ConfigService) => new PaymentGateway(config.get('KEY')),
inject: [ConfigService],
},
],
})
export class BillingModule {}Nest resolves most dependencies by the class type. But when a provider is registered under a string or symbol token (like a config value or an interface), there's no class to key on, so you inject it with @Inject('TOKEN').
Tokens matter for injecting non-class values and for depending on an interface: TypeScript interfaces vanish at runtime, so you register the implementation under a token and inject that token.
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class Mailer {
constructor(@Inject('API_KEY') private readonly apiKey: string) {}
}A dynamic module is a module configured at import time by a static method that returns a module definition. That's why you write ConfigModule.forRoot(options) or TypeOrmModule.forFeature([Entity]) instead of a bare import.
The pattern lets a reusable module accept configuration: forRoot sets up the module once for the whole app, forFeature registers per-feature pieces (like the entities a given module uses). It's how library modules stay configurable.
@Module({})
export class DatabaseModule {
static forRoot(uri: string): DynamicModule {
return {
module: DatabaseModule,
providers: [{ provide: 'DB_URI', useValue: uri }],
exports: ['DB_URI'],
};
}
}@Global() marks a module so its exported providers are available everywhere without importing the module in each place. It's used for things needed app-wide, like configuration.
Avoid it for most feature modules. Global scope hides dependencies and makes the graph harder to reason about, so the explicit import-and-export path is better unless a provider is genuinely used almost everywhere.
With TypeORM, you import TypeOrmModule.forRoot() with the connection config once, then TypeOrmModule.forFeature([Entity]) in each feature module to inject that entity's repository. With Prisma, you wrap the generated PrismaClient in an injectable PrismaService and inject it.
Either way, data access lives in providers behind an injectable, so services depend on a repository or client rather than talking to the database directly. That keeps the data layer swappable and testable.
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly repo: Repository<User>,
) {}
findAll() {
return this.repo.find();
}
}forRootAsync configures a dynamic module using values that aren't known statically, most often config loaded at runtime. Instead of passing a literal options object, you pass a factory that can inject ConfigService and return the options.
You need it whenever a module's setup depends on environment values: a database URL, an API key, a feature flag. Passing them through a synchronous forRoot would mean hardcoding, which defeats configuration.
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
url: config.get('DATABASE_URL'),
autoLoadEntities: true,
}),
});You use createParamDecorator, which receives the execution context and returns whatever value you want bound to the parameter. The classic example is a @CurrentUser() decorator that pulls the authenticated user off the request.
It cleans up controllers: instead of reading req.user in every handler, you write @CurrentUser() user: User, and the extraction logic lives in one place.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest();
return req.user;
},
);
// usage: findMe(@CurrentUser() user: User) { ... }You attach metadata to a route with a custom decorator built on SetMetadata (a @Roles('admin') decorator), then write a guard that reads that metadata with the Reflector and compares it to the current user's roles.
This is the standard Nest authorization pattern: the decorator declares the requirement next to the route, and one guard enforces it everywhere, so the rule and the check stay in sync.
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', ctx.getHandler());
if (!roles) return true;
const { user } = ctx.switchToHttp().getRequest();
return roles.some((r) => user.roles?.includes(r));
}
}@nestjs/passport wraps the Passport library in Nest's model. You write a strategy (JwtStrategy, LocalStrategy) whose validate() method returns the user, and Nest attaches it to the request. A guard (AuthGuard('jwt')) triggers the strategy on protected routes.
The flow: a login route issues a JWT with @nestjs/jwt, and protected routes use the JWT guard, which runs the strategy to verify the token and populate req.user. Keeping the strategy and guard split is the idiomatic setup.
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('JWT_SECRET'),
});
}
async validate(payload: any) {
return { userId: payload.sub, email: payload.email };
}
}Nest calls lifecycle hooks at defined moments. onModuleInit runs once the host module's dependencies are resolved, good for setup like opening a connection. onApplicationBootstrap runs after all modules are ready. onModuleDestroy and beforeApplicationShutdown run during graceful shutdown.
You implement the matching interface and the method. The shutdown hooks matter in production for clean teardown: closing database pools and finishing in-flight work when the process gets a termination signal.
@Injectable()
export class QueueService implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.connect();
}
async onModuleDestroy() {
await this.disconnect();
}
}The @nestjs/swagger package reads your decorators and DTOs to build an OpenAPI spec and a Swagger UI. You set it up in main.ts with a DocumentBuilder, and decorators like @ApiTags and @ApiProperty enrich the output.
Because Nest is already decorator-driven, much of the documentation comes for free from your controllers and DTO classes, so the spec stays close to the actual code.
const config = new DocumentBuilder()
.setTitle('Users API')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);Nest provides CacheModule, which you can back with an in-memory store or Redis. You either inject the cache manager to get and set values directly, or apply the CacheInterceptor to cache GET responses automatically.
The interceptor path is the quick win for read-heavy endpoints; the injected manager gives you control for partial or computed caching. Redis is the choice once you run more than one instance so the cache is shared.
@nestjs/schedule adds cron jobs, intervals, and timeouts through decorators. You mark a provider method with @Cron() or @Interval() and Nest runs it on schedule once ScheduleModule.forRoot() is imported.
For heavier or retryable background work you reach past the scheduler to a queue like BullMQ via @nestjs/bullmq, which persists jobs and survives restarts. The scheduler is for simple recurring work in-process.
import { Cron } from '@nestjs/schedule';
@Injectable()
export class ReportsJob {
@Cron('0 2 * * *') // every day at 02:00
async nightlyReport() {
await this.reports.generate();
}
}You build the whole app with Test.createTestingModule({ imports: [AppModule] }), call createNestApplication(), and fire real HTTP requests at it with supertest. That exercises the full pipeline: routing, guards, pipes, and serialization.
You can still override providers to stub external systems while testing the real request path. E2E tests catch wiring bugs that unit tests miss, like a guard blocking a route it shouldn't.
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
const app = moduleRef.createNestApplication();
await app.init();
return request(app.getHttpServer())
.get('/users')
.expect(200);Use the ClassSerializerInterceptor with class-transformer decorators. Mark a field with @Exclude() on the entity or response class, and the interceptor strips it before the response is sent, so a password hash never leaks.
This keeps the filtering declarative and central rather than deleting fields by hand in every handler. You enable the interceptor globally and annotate the classes.
import { Exclude } from 'class-transformer';
export class UserEntity {
id: string;
email: string;
@Exclude()
passwordHash: string;
}
// with ClassSerializerInterceptor, passwordHash is removed from responsesadvanced rounds probe internals, architecture judgment, and production scars. Expect every answer here to draw a follow-up.
At startup Nest scans modules, reads the constructor metadata that TypeScript emits (via emitDecoratorMetadata), and builds a dependency graph. It topologically resolves that graph, instantiating each singleton once and injecting resolved instances into constructors.
Resolution is scoped by module: a provider token is looked up in the module and its imported, exported providers. This is why the 'can't resolve dependencies' error names the token and the module, and why request-scoped providers force a per-request sub-tree instead of the startup singleton pass.
Key point: The production signal is naming reflect-metadata and emitDecoratorMetadata as what makes constructor injection work. Candidates who treat DI as magic get exposed here.
Watch a deeper explanation
Video: NestJS in 100 Seconds (Fireship, YouTube)
When two providers (or modules) depend on each other, Nest can't resolve the graph and errors at startup. The escape hatch is forwardRef() on both sides, which defers resolution so the container can break the cycle.
The better answer is that a circular dependency is usually a design smell. Before reaching for forwardRef, consider extracting the shared logic into a third provider both depend on, or using an event to invert one direction of the call.
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => AuthService))
private authService: AuthService,
) {}
}
// mirror the forwardRef on AuthService's side tooKey point: Saying 'forwardRef works, but a cycle usually means the design should change' is what separates The production-ready answer from a Stack Overflow copy.
Nest has a microservices layer that reuses the same controllers and providers but swaps HTTP for a message transport. You create the app with createMicroservice and pick a transporter: TCP, Redis, NATS, RabbitMQ, Kafka, or gRPC.
Handlers change from @Get() to @MessagePattern() (request-response) or @EventPattern() (fire-and-forget). The architecture point to make: message patterns give you decoupling and back-pressure that raw HTTP calls between services don't.
| Transport | Style | Good fit |
|---|---|---|
| TCP | Request-response | Simple internal service-to-service |
| Redis / NATS | Pub-sub + req-res | Lightweight eventing, low latency |
| RabbitMQ / Kafka | Queues / streams | Durable messaging, high volume |
| gRPC | Typed RPC | Strict contracts, polyglot services |
@nestjs/graphql integrates Apollo (or Mercurius). You write resolvers the same way you write controllers, with @Resolver, @Query, and @Mutation. The two approaches differ in where the schema comes from.
Code-first generates the GraphQL schema from TypeScript classes and decorators, so types stay the single source of truth. Schema-first starts from a .graphql SDL file and generates types from it. Code-first suits TypeScript-heavy teams; schema-first suits contracts owned outside the code.
The @nestjs/cqrs module splits writes (commands) from reads (queries) and adds events, each with its own handler. It fits domains with rich business rules where you want commands, events, and side effects as first-class, testable units rather than fat services.
The honest caveat: it adds real ceremony. For CRUD-shaped features it's over-engineering. Reach for it when the domain logic is genuinely complex or when you want an event stream driving other work, not by default.
ArgumentsHost abstracts the underlying arguments of the current context so the same guard, interceptor, or filter works across HTTP, WebSockets, and microservices. You call switchToHttp(), switchToRpc(), or switchToWs() to get the right shape.
ExecutionContext extends it with getHandler() and getClass(), which is what lets a guard read route metadata via the Reflector. This abstraction is why one RolesGuard can protect an HTTP route and an RPC handler without rewriting.
canActivate(context: ExecutionContext) {
if (context.getType() === 'http') {
const req = context.switchToHttp().getRequest();
// HTTP-specific logic
}
const handler = context.getHandler(); // read route metadata
return true;
}app.useGlobalGuards(new MyGuard()) registers a guard outside the DI container, so it can't inject dependencies. Binding it as a provider with the APP_GUARD token registers it through DI, so it can inject services like a ConfigService or Reflector.
The same holds for APP_PIPE, APP_INTERCEPTOR, and APP_FILTER. The rule: if the global enhancer needs dependencies, use the token form; the useGlobalX form is only fine for enhancers with no dependencies.
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{ provide: APP_GUARD, useClass: RolesGuard }, // DI-aware global guard
],
})
export class AppModule {}Key point: This exact distinction is a favorite senior question. 'My global guard can't inject anything' is almost always someone using useGlobalGuards when they needed APP_GUARD.
A request-scoped provider is rebuilt on every request instead of once at startup, and the scope is contagious: any provider that injects a request-scoped one becomes request-scoped too, and so on up the chain. A single misplaced request-scoped provider can turn much of your graph per-request.
That means more allocations and slower requests, and it can silently disable performance features that assume singletons. The strong move is to keep providers singleton and pass per-request data explicitly, using the injected REQUEST only where truly needed.
Provider instantiations to serve 10,000 requests
By definition a singleton is built once at startup; a request-scoped provider is rebuilt per request. This is a definitional count, not a benchmark.
You call app.enableShutdownHooks() so Nest listens for termination signals and runs the shutdown lifecycle. Then providers implement onModuleDestroy or beforeApplicationShutdown to close database pools, drain queues, and stop accepting new work.
The production detail: pair this with the platform stopping traffic first (a readiness probe flipping to not-ready), so in-flight requests finish while new ones stop arriving. Enabling shutdown hooks without draining traffic still drops requests.
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();
await app.listen(3000);
// providers with onModuleDestroy() now run on SIGTERM/SIGINTThree layers. Unit tests resolve a single provider with its dependencies mocked, fast and numerous. Integration tests build a real module (a service plus its repository against a test database) to catch wiring and query bugs. E2E tests hit the running app over HTTP to verify the full pipeline including guards and pipes.
The judgment interviewers probe is what to mock where: mock external boundaries (payment APIs, email) always, use a real database in integration tests, and override providers with Nest's testing utilities rather than reaching into internals.
The CLI has a monorepo mode with multiple apps and shared libraries under one workspace, configured in nest-cli.json. Shared code (DTOs, common guards, a database module) lives in a library that each app imports, so contracts stay in one place.
At larger scale teams often move to Nx for smarter caching and dependency graphs, but the built-in monorepo covers the common case: several Nest services sharing typed contracts without publishing internal packages.
Define a domain exception hierarchy (a base app exception plus specific subclasses), throw those from services, and use a single global exception filter to map them to HTTP responses with a stable shape. Reserve Nest's built-in HTTP exceptions for the edge, and translate domain errors at the boundary.
The filter is also where you attach a request id, log with context, and avoid leaking internals in the message. Centralizing this means every endpoint returns errors the same way, which clients and on-call engineers rely on.
class-transformer turns a plain incoming object into an instance of the DTO class (that's the transform option on ValidationPipe). class-validator then reads the validation decorators on that instance and checks each rule, collecting errors.
The order matters: transform first so nested objects and types are real class instances, then validate. Options like enableImplicitConversion coerce types (a string '5' to a number) and whitelist strips unknown properties, which together stop over-posting attacks.
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true, // 400 on unknown props instead of stripping
transformOptions: { enableImplicitConversion: true },
});A gateway is a class decorated with @WebSocketGateway() that handles real-time events. @SubscribeMessage('event') maps an incoming message to a handler, and you emit back to clients through the injected server or client socket.
The design point: guards, interceptors, and pipes still apply through the same ArgumentsHost abstraction, so authorization and validation reuse the HTTP-side components. Socket.io is the default adapter; ws is an option.
@WebSocketGateway()
export class ChatGateway {
@WebSocketServer() server: Server;
@SubscribeMessage('message')
handleMessage(@MessageBody() text: string) {
this.server.emit('message', text); // broadcast
}
}Replace the default logger with a structured one (pino via nestjs-pino) so logs are JSON with request context. Add an interceptor to time requests and emit metrics, and wire OpenTelemetry so spans propagate across service calls and into the database driver.
The senior framing is that Nest's interceptor and DI model make observability a cross-cutting concern you add in one place, not per-handler. Correlating a request id from the edge through logs, traces, and downstream calls is what makes production debugging tractable.
feature modules that own their controllers, services, and DTOs, and depend on each other only through exported providers, so boundaries stay explicit comes first. Put cross-cutting concerns (auth, logging, config) in shared modules bound globally through APP_GUARD-style tokens. Keep the data layer behind repositories so it stays swappable.
Then address scale: keep providers singleton for performance, push heavy or retryable work to queues, split into microservices only when a domain boundary and team ownership justify it, and enforce the module boundaries in review. Structuring the answer modules, cross-cutting concerns, data layer, then scale is what the question rewards.
Key point: This is an architecture question wearing a NestJS costume. The technical decision depends on whether you reason from boundaries and ownership, not whether you recite decorators.
NestJS wins when a team wants structure enforced rather than agreed on: it gives Node the module, DI, and decorator conventions that stop large codebases from drifting. It trades a steeper learning curve and more boilerplate for that consistency. Plain Express or Fastify stay lighter and faster to start, but leave architecture to the team, which shows once several developers touch the same repo. Spring Boot occupies the same structured-framework space in the JVM world, so the honest comparison is Nest is to Node what Spring Boot is to Java. Saying these trade-offs out loud is itself an interview signal.
| Framework | Language | Structure | Best at |
|---|---|---|---|
| NestJS | TypeScript | Opinionated (modules, DI, decorators) | Large team Node apps that need consistency |
| Express | JavaScript | Minimal, unopinionated | Small services, full control, fast start |
| Fastify | JavaScript | Minimal, plugin-based | High-throughput Node APIs, low overhead |
| Spring Boot | Java/Kotlin | Opinionated (beans, DI, annotations) | Enterprise JVM backends |
Prepare in layers, and practice out loud. Most NestJS rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers.
The typical NestJS interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These NestJS questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works