TL;DR: NestJS interviews in 2026 test dependency injection, the request pipeline of guards, interceptors and pipes, provider scopes and microservice transports. Candidates should know that NestJS 12 ships every core package as ESM and needs Node.js 20.19 or 22.12 and later. It also validates with Standard Schema libraries such as Zod.
NestJS 12.0 shipped on August 27, 2026, and moved every core package to ESM. Existing CommonJS apps keep running through Node's require(esm), so most teams upgrade without rewriting imports.
The DI container, the request pipeline and the module system work as before, and they still decide most interviews.
- 1Current versions as of September 2026: @nestjs/core 12.1.0 and @nestjs/cli 12.0.6. The 11.x line still gets patch releases.
- 2NestJS 11 moved to Express 5, so a bare
*wildcard in a route path must now be named, as in*splat. - 3NestJS 12.1 added built-in CSRF protection and security headers, with no extra package needed.
- 4Cache and rate-limit TTLs are in milliseconds, not seconds, in current
@nestjs/cache-managerand@nestjs/throttler.
Core and DI
1. What does the NestJS dependency injection container do?
It creates your classes and hands each one the dependencies its constructor asks for. You never call new UsersService(repo) yourself. You mark the class with @Injectable() and list it in a module's providers.
Nest builds it once and injects it wherever it is needed.
Nest reads constructor parameter types from TypeScript metadata to find each dependency. That is why the classes must be real classes, not interfaces. An interface disappears at compile time, so it needs a custom token and @Inject(TOKEN).
@Injectable()
export class UsersService {
constructor(private readonly repo: UsersRepository) {}
}
The payoff is testing. A test module can swap UsersRepository for a fake without touching UsersService.
2. What are the four kinds of custom provider?
Nest supports useClass, useValue, useFactory and useExisting.
useClass: bind a token to a class, for example a different implementation per environment.useValue: bind a token to a ready-made value, such as a config object or a mock in tests.useFactory: build the value in a function that can itself inject other providers and can be async. Database connections are the usual case.useExisting: an alias, so two tokens resolve to the same instance.
{
provide: 'DB',
useFactory: async (config: ConfigService) => connect(config.get('DATABASE_URL')),
inject: [ConfigService],
}
3. Why does Nest say it "can't resolve dependencies" of a provider?
The provider asks for something that is not visible in its module. Providers are private to the module that declares them. To share one, the owning module lists it in exports. The consuming module then lists the owning module in imports.
The error message names the missing argument by its index. The usual fixes are to export the provider, import the module, or remove a duplicate registration of the same class in two modules.
Two registrations create two separate instances, which is a quiet bug of its own.
4. What is a dynamic module, and when do you write one?
A dynamic module takes options when it is imported. A static method such as forRoot() or register() returns a DynamicModule object. TypeOrmModule.forRoot() and ConfigModule.forRoot() are the familiar examples.
Write one when a module must behave differently in different apps, such as a mail client that needs an API key. The async variant, forRootAsync(), reads its options from other providers.
The ConfigurableModuleBuilder class generates both variants and the options token for you, so hand-written boilerplate is rarely needed now. See the dynamic modules chapter.
5. How do you handle a circular dependency?
Break it first; use forwardRef() only when you cannot. A cycle between two services usually means a third concern is hiding inside both of them. Moving that code into its own provider removes the cycle.
When a cycle is real, wrap the injected class in forwardRef(() => OtherService) on both sides. Use the same helper in imports when two modules depend on each other.
The docs warn that the order of instantiation is then not guaranteed, so neither constructor should rely on the other being ready. Event-based decoupling with @nestjs/event-emitter is another way out.
6. What are the three injection scopes, and what does request scope cost?
Scope is set on the provider with @Injectable({ scope }), not on the module. There are three:
Scope.DEFAULT: one shared instance for the whole app. Use this almost always.Scope.REQUEST: a new instance for every incoming request, garbage-collected afterwards.Scope.TRANSIENT: a new instance for every class that injects it.
The cost is that request scope bubbles up. A controller that injects a request-scoped service becomes request-scoped too, and so does everything in between.
The injection scopes chapter says this slows average response times and recommends the default scope unless you truly need otherwise.

For multi-tenant apps, durable providers are the middle ground. A ContextIdStrategy groups requests by tenant, so Nest builds one sub-tree per tenant instead of one per request.
7. Which lifecycle hooks does Nest call, and why might shutdown hooks never fire?
On startup Nest calls onModuleInit() and then onApplicationBootstrap(). On shutdown it calls onModuleDestroy(), beforeApplicationShutdown() and onApplicationShutdown().
The shutdown hooks only run on app.close() or on a signal such as SIGTERM if you called app.enableShutdownHooks() at bootstrap. The listeners are off by default.
Without that call, a Kubernetes pod can stop with open database connections and unfinished jobs.
Request Pipeline
8. In what order do middleware, guards, interceptors, pipes and filters run?
Middleware runs first, then guards, then interceptors, then pipes, then the handler. After the handler, interceptors run again on the way out. Exception filters catch anything thrown along the way.

Within each stage, global components run before controller ones, and controller ones before route ones. Filters go the other way: route filters get the first chance to catch, and global filters come last.
The full order is in the request lifecycle FAQ.
9. When do you use a guard instead of middleware?
Use a guard for authorization, because a guard knows which handler will run next and middleware does not. A guard receives an ExecutionContext, so it can read metadata on the handler, such as the roles a route requires.
Middleware still fits work that does not depend on the route. Request IDs, logging and parsing a token are good examples. A common split is: middleware or a Passport strategy authenticates, and a guard authorizes.
10. How would you build a role check with a custom decorator?
Create the decorator with Reflector.createDecorator(), attach it to routes, and read it in a guard. This is the approach the guards chapter now shows first; @SetMetadata() still works as a lower-level option.
export const Roles = Reflector.createDecorator<string[]>();
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const roles = this.reflector.getAllAndOverride(Roles, [
ctx.getHandler(),
ctx.getClass(),
]);
if (!roles) return true;
const { user } = ctx.switchToHttp().getRequest();
return roles.some((r) => user?.roles?.includes(r));
}
}
getAllAndOverride lets a method-level @Roles() replace a class-level one. A strong candidate also mentions a @Public() decorator for routes that skip a global auth guard.
11. What do the main ValidationPipe options do?
whitelist strips any property that has no validation decorator on the DTO. forbidNonWhitelisted rejects the request instead of stripping. transform turns the plain payload into an instance of the DTO class.
It also converts primitive types, so a route param typed as number arrives as a number.
app.useGlobalPipes(
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
);
Without whitelist, a client can send extra fields such as isAdmin. Those fields may then reach the database through a spread into an update call. That mass-assignment bug is the main reason to turn it on globally.
12. What can an interceptor do that a guard or pipe cannot?
An interceptor wraps the handler, so it can act both before and after it runs. The handler's result arrives as an RxJS stream from next.handle(). The interceptor can map it, time it, cache it, retry it or add a timeout.
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler) {
const start = Date.now();
return next.handle().pipe(
tap(() => console.log(`${Date.now() - start}ms`)),
);
}
}
If the interceptor returns a value without calling next.handle(), the handler never runs. That is how CacheInterceptor serves a cached response.
13. How should errors reach the client?
Throw an HttpException subclass, such as NotFoundException, and let the built-in exception layer format the response. Any error that is not an HttpException becomes a generic 500 with no internal details.
A custom filter with @Catch() changes the format, for example to map a database unique-constraint error to a 409. Keep domain services free of HTTP exceptions where you can.
A service that throws NotFoundException is hard to reuse from a queue consumer or a gRPC handler.
Data and Performance
14. How does caching work in current NestJS?
Register CacheModule from @nestjs/cache-manager. Then either bind CacheInterceptor to cache GET responses, or inject the cache and call get and set yourself.
The caching chapter notes that the store is built on Keyv, so Redis and other backends plug in as Keyv stores.
Two details trip up candidates who learned older versions. TTL is in milliseconds. And the in-memory store serializes values to JSON. A Date comes back as a string, and a class instance comes back as a plain object.
15. How do you add rate limiting?
Use @nestjs/throttler: configure ThrottlerModule with a ttl in milliseconds and a limit, then bind ThrottlerGuard globally. You can define several named limits, such as 3 per second and 100 per minute.
Adjust them per route with @Throttle(), or skip them with @SkipThrottle().
The default storage is in memory, so each instance counts on its own. Behind a load balancer with several pods, a shared store such as Redis is needed for one real limit.
Behind a proxy, the app must also read the client IP correctly, or every request looks like it comes from the proxy.
16. Where should database access live in a NestJS app?
In providers, never in controllers. With TypeORM, inject a repository with @InjectRepository(User). With Prisma, wrap the client in a PrismaService provider and inject that.
Many teams add their own repository class on top, such as UsersRepository. Services then call methods like findActiveByEmail() rather than ORM code. It makes unit tests simpler and keeps query logic in one place.
The trade-off is one more layer to maintain, so it pays off mostly in larger codebases.
17. How do you handle configuration and secrets?
Use @nestjs/config: ConfigModule.forRoot() loads environment variables, and ConfigService reads them wherever they are needed. Validate them at startup so a missing DATABASE_URL fails the boot instead of the first request.
ConfigModule.forRoot({
isGlobal: true,
validationSchema: z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
}),
});
Since @nestjs/config 12, validationSchema takes any Standard Schema library, and Zod is the recommended choice for new projects. Joi still works from version 18.
18. When would you switch from the Express adapter to Fastify?
Switch when throughput per instance matters and the app does not depend on Express-only middleware. Nest runs on either through @nestjs/platform-express or @nestjs/platform-fastify, and controllers do not change.
The cost shows up at the edges. Express middleware, multer uploads and code that reaches into req and res directly need Fastify equivalents. NestJS 12.1 narrowed that gap with file upload interceptors backed by @fastify/multipart.
A good answer says to benchmark the real app, since database time often dwarfs framework overhead.
19. An endpoint is slow under load. How do you find the cause?
Measure before changing anything. Start with traces or timing logs to split the request into database time, outbound calls and CPU time in Node.
- Database: log queries and look for N+1 patterns, such as lazy relations loaded in a loop, and for missing indexes.
- Event loop: CPU-heavy work, such as large JSON transforms or hashing, blocks every request. Move it to a worker thread or a queue.
- Nest itself: check for request-scoped providers that force whole controller trees to be rebuilt on every request.
Only then add caching, and cache the slow part rather than the whole response when data changes often.
Microservices and CQRS
20. What is the difference between ClientProxy send() and emit()?
send() is request-response and returns a cold Observable, so nothing is sent until you subscribe. emit() publishes an event and returns a hot Observable, so the message goes out right away whether or not you subscribe.
On the receiving side, send() pairs with @MessagePattern(), which returns a reply. emit() pairs with @EventPattern(), which returns nothing.
A classic bug is calling send() without subscribing or awaiting it with firstValueFrom(), so the message is never sent. See the microservices chapter.
21. How do you choose a microservice transporter?
Pick it by the delivery guarantee you need, not by the Nest API, which is the same for all of them. Nest ships TCP, Redis, NATS, MQTT, RabbitMQ, Kafka and gRPC transporters.
- gRPC: typed request-response between services, with a
.protocontract. - RabbitMQ: work queues with acknowledgements, so a job survives a crashed consumer.
- Kafka: a durable, replayable event log with ordering per partition.
- Redis or NATS: light pub/sub. Core Redis pub/sub drops messages when no subscriber is listening.
TCP is the default but suits little beyond local experiments, since it has no broker to buffer messages.
22. What does @nestjs/cqrs give you, and when is it worth it?
It gives you a CommandBus, a QueryBus and an EventBus, with one handler class per command, query or event. Sagas react to events by sending new commands.
It is worth it when reads and writes have different shapes or scaling needs, or when you already model the domain as events. For a CRUD service it adds a class per operation and indirection with little gain.
A good candidate says that CQRS does not require event sourcing; the two are often confused.
Testing
23. How do you unit test a service with its dependencies mocked?
Build a small module with Test.createTestingModule(), replace the real dependencies with fakes, and fetch the service with get().
const moduleRef = await Test.createTestingModule({
providers: [
UsersService,
{ provide: UsersRepository, useValue: { findById: vi.fn() } },
],
}).compile();
const service = moduleRef.get(UsersService);
For a module that is already wired up, overrideProvider(X).useValue(fake) swaps one provider and keeps the rest. @nestjs/testing works with any runner. New ESM projects default to Vitest in NestJS 12, and CommonJS projects keep Jest.
24. How do e2e tests differ from unit tests in Nest?
An e2e test boots the real application module, calls app.init(), and sends HTTP requests with supertest against app.getHttpServer(). Guards, pipes, filters and interceptors all run, which unit tests skip.
Point e2e tests at a real database, usually in a container, and reset it between tests. Mocking the database in e2e tests hides exactly the query and constraint bugs they are meant to catch.
Remember to register the same global pipes and prefixes as main.ts, or the test app behaves differently from production.
What Changed Recently
QUERY method supportnest upgrade25. What does upgrading to NestJS 12 involve?
For most apps it is a Node.js check and one command. The 12.0.0 release notes say every core package now ships as ESM. A CommonJS app loads them through require(esm), which needs Node.js 20.19+ or 22.12+; the 21.x line is not supported.
Upgrade the global CLI, then run nest upgrade (try --dry-run first). It moves every @nestjs/* package to its v12 major and applies the mechanical changes. It leaves your module format, test runner and linter alone.
The migration guide adds two traps: Jest can load the ESM packages only on Node.js 24.9 or later, and AWS Lambda needs --experimental-require-module in NODE_OPTIONS.
26. How does Standard Schema validation work in NestJS 12?
Route parameter decorators take a schema option, and a global StandardSchemaValidationPipe validates against it. Any library that implements Standard Schema works, including Zod, Valibot and ArkType.
@Get(':id')
findOne(@Param('id', { schema: z.coerce.number().int().positive() }) id: number) {
return this.users.findOne(id);
}
The decorator only attaches metadata; without the pipe nothing is checked. StandardSchemaSerializerInterceptor does the same for responses.
The class-validator flow with ValidationPipe is still fully supported, so this is a choice, not a migration.
- Class DTOs with class-validator decorators
ClassSerializerInterceptorfor responses- Fits existing codebases
- Schemas from Zod, Valibot or ArkType
StandardSchemaSerializerInterceptorfor responses- New in NestJS 12
27. What broke in routes when NestJS 11 moved to Express 5?
Express 5 changed how paths match. According to the NestJS 11 announcement, a wildcard must now have a name, as in users/*splat.
The optional ? character is gone in favour of braces, as in /:file{.:ext}, and regular expression characters are no longer supported in paths.
Nest converts old users/* routes automatically, but the post advises against relying on that. Note also that *splat does not match the bare root, /users; use users/{*splat} for that.
28. What security features did NestJS 12.1 add?
Three, per the 12.1.0 release: built-in CSRF protection, security headers and adapter-agnostic cookies. All work the same on Express and Fastify with no extra package.
app.enableCsrfProtection() rejects state-changing requests that the browser marks as cross-origin through the Sec-Fetch-Site and Origin headers.
It needs no tokens or session, and it follows Go 1.25's CrossOriginProtection algorithm. app.useSecurityHeaders() sets the same defaults as Helmet 8. Both must be called before app.listen(), ideally right after NestFactory.create().
See the CSRF chapter.
29. How has logging and error output changed since NestJS 11?
NestJS 11 added JSON output to the built-in ConsoleLogger with new ConsoleLogger({ json: true }), which suits container log collectors. NestJS 12 made plain objects passed after the message part of the same log entry, nested under params.
logger.log('User created', { userId: 1 });
throw new BadRequestException('Password is too weak', { errorCode: 'WEAK_PASSWORD' });
NestJS 12 also lets an HttpException carry an errorCode in the response body. Clients can then branch on a stable code instead of parsing message text. Set structuredParams: false to restore the old logging output.
30. Which NestJS 12 behaviour changes can break an app that compiles?
Three are worth knowing, all listed in the migration guide:
- Lifecycle hook order: hooks such as
onModuleInitnow run by level in the component hierarchy, so code that relied on a specific order between related providers may change behaviour. @Optional()is no longer inherited: a subclass without its own constructor now throwsUnknownDependenciesExceptionwhere v11 injectedundefined.- GraphQL subscriptions:
subscriptions-transport-wssupport is removed. Clients must move tographql-ws, whose protocol is not wire-compatible.
A candidate who has done the upgrade will usually mention at least one of these without prompting.
Signs of a Strong Answer
- They can recite the request order and say which stage they would put a given concern in, such as tenant lookup or response mapping.
- They warn that request scope bubbles up through every injecting class, and suggest AsyncLocalStorage or durable providers instead.
- They turn on
whitelistinValidationPipeand can explain the mass-assignment bug it prevents. - They know
send()is cold andemit()is hot, and which transporters lose messages when no consumer is listening. - They keep HTTP exceptions out of domain services so the same code serves HTTP, queues and gRPC.
- They can say what
nest upgradechanged in a real project, and which Node.js version their tests run on.
Hiring NestJS Developers
NestJS sits on top of Node.js and TypeScript, so the strongest candidates are good Node engineers first. Second Talent matches companies with pre-vetted Node.js developers, including NestJS specialists, screened with questions like these.
Tell us the stack and we send a shortlist within 24 hours. Start hiring, or see our Node.js and microservices interview guides.






