NestJS Interview Questions and Answers

Last updated:

Check out 30 of the most common NestJS interview questions, then take an AI-powered practice interview

TypeScriptNode.jsDecoratorsMicroservicesGraphQL
30+
Questions
12
Basic
13
Intermediate
5
Advanced
Q1

What is NestJS and what problem does it solve?

BasicFundamentals

Answer

NestJS is a progressive Node.js framework for building efficient, scalable, server-side applications using TypeScript. Released in 2017 by Kamil Mysliwiec, it sits on top of Express (the default HTTP adapter) or Fastify, and adds a structured architecture inspired by Angular: modules, controllers, providers, decorators, and a powerful dependency injection container. The problem NestJS solves is the lack of an opinionated structure in plain Express or Koa apps.

Teams that start with Express often end up with a tangle of routes, middleware, and hand-rolled singletons that becomes hard to test and maintain past a few thousand lines of code. NestJS replaces that with clear separation of concerns: controllers handle HTTP, services hold business logic, modules wire things together, and the DI container manages lifecycles. It also unifies HTTP, WebSockets, GraphQL, and microservices behind one programming model, so you do not need to learn three different patterns for one application. In India, this single-paradigm advantage is what convinces engineering managers at fintech and SaaS firms to standardise on Nest, a new joiner who knows the framework can navigate any service in the organisation, whether it is a REST API, a Kafka consumer, or a GraphQL gateway.

Key Points

  • TypeScript-first framework built on Express or Fastify
  • Angular-inspired modules, controllers, providers, and DI
  • Unified model for HTTP, GraphQL, WebSockets, and microservices
  • Strong testability through constructor-based dependency injection
  • Standardised across services, so engineers ramp up faster
Q2

What are modules, controllers and providers in NestJS?

BasicArchitecture

Answer

These are the three core building blocks of every NestJS application. A module is a class annotated with @Module() that groups related controllers and providers, declares its imports and exports, and forms a feature boundary. Every Nest application has at least one root AppModule, and most non-trivial codebases split into feature modules such as UsersModule, AuthModule, and PaymentsModule, each owning its own controllers and services.

A controller is annotated with @Controller() and handles incoming HTTP requests, mapping URL paths and HTTP verbs to handler methods using decorators like @Get, @Post, @Patch, @Param and @Body. A provider is any class that can be injected as a dependency, typically a service annotated with @Injectable() that contains business logic, but also repositories, factories, helpers, configuration objects, or even values. The DI container instantiates providers as singletons by default and injects them into controllers and other providers via the constructor. Keeping controllers thin and pushing logic into services is the canonical NestJS pattern and the single biggest indicator of code quality interviewers look for during code reviews and pair programming rounds.

import { Module, Controller, Injectable, Get } from '@nestjs/common';

@Injectable()
export class UsersService {
  findAll() {
    return [{ id: 1, name: 'Saksham' }];
  }
}

@Controller('users')
export class UsersController {
  constructor(private readonly users: UsersService) {}

  @Get()
  list() {
    return this.users.findAll();
  }
}

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // expose to other modules
})
export class UsersModule {}
💡 Pro Tip: Treat controllers as a thin HTTP shell. If you find business logic creeping into a controller method, extract it into a service immediately, controllers should be no more than ten or fifteen lines per method.
Q3

How does dependency injection work in NestJS?

BasicDependency Injection

Answer

NestJS has its own IoC (inversion of control) container. When the application bootstraps, the container scans every module, reads the metadata emitted by TypeScript decorators (via the reflect-metadata package), and builds a dependency graph. When you declare a class with @Injectable() and add it to a module's providers array, you can constructor-inject it into any other provider or controller in that module or in modules that import it.

By default every provider is a singleton at the application level, so the same instance is reused for every request. You do not call new yourself, the container instantiates each provider once and wires up its dependencies recursively. This is the foundation that makes NestJS so testable: in unit tests, you replace a real provider with a mock using the Test.createTestingModule() builder, and the container injects the mock instead of the production class.

The same machinery powers advanced patterns such as custom providers (useValue, useFactory, useClass, useExisting), async providers (where the factory is asynchronous), and injection tokens (string or symbol keys for interface-like injection). Understanding this graph deeply is what separates a beginner from a senior NestJS engineer, almost every advanced topic eventually reduces to a question about provider resolution.

Key Points

  • Container reads metadata from @Injectable() and @Module()
  • Constructor injection is the default pattern
  • Singletons by default; scope can be REQUEST or TRANSIENT
  • useValue, useFactory, useClass, useExisting for advanced wiring
  • Critical for testability, providers are swappable in tests
Q4

How do you handle route parameters, query strings and request bodies?

BasicRouting

Answer

NestJS provides parameter decorators that extract the right piece of the request and pass it to your handler argument. @Param() reads URL path parameters, @Query() reads the query string, @Body() reads the parsed JSON body, @Headers() reads HTTP headers, and @Req() / @Res() give you the underlying Express or Fastify request and response if you need them. You can extract a specific key or the whole object, @Param('id') gives you just the id, while @Param() gives you the entire params object. Combine these with class-validator DTOs and the global ValidationPipe to automatically validate and transform inputs at the boundary, so by the time data reaches your service, it is already typed and trustworthy.

Avoid reaching for @Res() unless you absolutely need to set custom headers or stream a response, once you do, you lose the benefit of returning a value from your handler, and Nest no longer applies interceptors to the response. For file uploads use @UploadedFile() (or @UploadedFiles() for multiple) combined with FileInterceptor and the multer engine, which Nest bundles automatically. For very large uploads, stream to S3 directly using the AWS SDK's multipart upload API instead of buffering in Node memory, and validate the MIME type up front to reject malicious payloads before they touch your storage layer.

import { Controller, Get, Post, Param, Query, Body, ParseIntPipe } from '@nestjs/common';

@Controller('items')
export class ItemsController {
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number, @Query('lang') lang = 'en') {
    return { id, lang };
  }

  @Post()
  create(@Body() body: CreateItemDto) {
    return { created: body };
  }
}

Key Points

  • Use @Param, @Query, @Body, @Headers parameter decorators
  • Combine with ValidationPipe for type safety at the boundary
  • Avoid @Res() unless absolutely necessary; it disables interceptors
  • Use @UploadedFile() with FileInterceptor for multipart uploads
Q5

What is a DTO and how does class-validator integrate with NestJS?

BasicValidation

Answer

A DTO (Data Transfer Object) is a TypeScript class that describes the shape of a request payload. Combined with the class-validator and class-transformer packages, DTOs become the runtime validation layer for every incoming request. You decorate each property with rules like @IsString(), @IsEmail(), @IsInt(), @Min(0), @IsOptional(), @ValidateNested(), then register the global ValidationPipe on the app.

NestJS deserializes the incoming JSON into the DTO class, runs validators, and returns a 400 with structured error details if anything fails. You should always enable whitelist: true and forbidNonWhitelisted: true to strip and reject unknown fields, this is your first line of defence against mass-assignment vulnerabilities where an attacker sends extra fields like isAdmin: true hoping your code passes the payload straight to the database. With transform: true, query strings are coerced from strings to their declared TypeScript types before reaching the handler, so a parameter typed as number will arrive as a real number, not '42'. In production at Razorpay-style fintech, DTO validation is treated as a security boundary, not a convenience, every payload that crosses the network is a DTO with strict whitelisting and explicit length / range limits.

import { IsEmail, IsInt, Min, IsOptional, MaxLength } from 'class-validator';
import { ValidationPipe } from '@nestjs/common';

export class CreateUserDto {
  @IsEmail()
  @MaxLength(254)
  email: string;

  @IsInt()
  @Min(13)
  age: number;

  @IsOptional()
  @MaxLength(80)
  fullName?: string;
}

// main.ts
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,
  forbidNonWhitelisted: true,
  transform: true,
  transformOptions: { enableImplicitConversion: true },
}));
💡 Pro Tip: Always set transform: true so query strings like ?page=2 are coerced to numbers before reaching your handler.
Q6

What is the difference between @Controller and @Injectable?

BasicDecorators

Answer

Both decorators register a class with the Nest DI container, but they serve different roles. @Controller() marks a class as an HTTP request handler, Nest reads its method-level decorators (@Get, @Post, etc.) and registers routes with the underlying HTTP adapter. Controllers are not injectable into other providers; they are an entry point, not a service. @Injectable() marks a class as a provider, meaning it can be injected wherever the DI container resolves dependencies, into controllers, into other services, into guards, pipes, interceptors, and even into resolvers in GraphQL modules. As a rule of thumb: controllers are thin HTTP shells, while @Injectable() services hold the business logic that controllers call.

Mixing them is a common anti-pattern that produces controllers difficult to unit-test, and during interviews this is often the first signal a candidate has not internalised the framework's philosophy. When in doubt, ask yourself whether the class will be reused, anything used in more than one place should be an @Injectable() service. The same separation applies to other entry points: @Resolver() for GraphQL, @WebSocketGateway() for WebSockets, and @Catch() for exception filters are all entry-point decorators that should stay thin and delegate to injectable services for the actual work. Get this right and your tests become trivial; get it wrong and every controller test becomes a slow integration test.

Key Points

  • @Controller marks an HTTP entry point; @Injectable marks a reusable provider
  • Controllers should be thin and delegate to services
  • Services can be injected anywhere; controllers cannot be injected
  • Same pattern applies to @Resolver, @WebSocketGateway, @Catch
Q7

How do you generate a new NestJS project and module?

BasicTooling

Answer

The Nest CLI (@nestjs/cli) is the canonical way. Install it globally with npm i -g @nestjs/cli, then run nest new project-name to scaffold a new project with TypeScript, Jest, ESLint, Prettier, and a working src/main.ts already configured. Inside an existing project, nest generate (or nest g for short) scaffolds individual pieces: nest g module users creates a UsersModule, nest g controller users creates the controller and wires it into the module, nest g service users does the same for the service.

The CLI also supports nest g resource users which generates the whole CRUD bundle, controller, service, module, DTOs, and entity, in one command, and even asks whether you want a REST, GraphQL code-first, GraphQL schema-first, or microservice variant. In team environments the CLI is strongly preferred because it keeps file naming, exports, and module wiring consistent across developers, so a code review never devolves into arguing about where a file should live. The CLI also writes proper unit and e2e test stubs alongside each generated file, which nudges everyone toward writing tests from day one. For monorepos, nest new --strict supports a workspace flavour where multiple apps and libraries share the same node_modules and TypeScript config, useful when you are running a frontend in Next.js and a backend in NestJS within the same Turborepo or Nx workspace, a common 2026 setup at Indian SaaS startups.

# Project scaffolding
npm i -g @nestjs/cli
nest new my-api --package-manager pnpm

# Inside an existing project
nest g module users
nest g controller users
nest g service users

# Full CRUD resource (controller + service + module + DTOs)
nest g resource billing

# Run with hot reload
npm run start:dev
Q8

What are pipes in NestJS and what are they used for?

BasicPipes

Answer

Pipes are classes annotated with @Injectable() that implement the PipeTransform interface. Nest runs them between the framework and the handler method, giving you a hook for two things: transformation (turning a string '123' into the number 123) and validation (throwing a BadRequestException if the input is invalid). The framework ships with built-ins like ParseIntPipe, ParseUUIDPipe, ParseBoolPipe, ParseArrayPipe, ParseEnumPipe and the ValidationPipe.

You can also write your own, for example, a TrimPipe that strips whitespace from query parameters, or a SlugifyPipe that normalises user-supplied strings. Apply pipes at the parameter level (@Param('id', ParseIntPipe)), the handler level via @UsePipes(), the controller level, or globally on the app. Per-parameter pipes are the cleanest pattern because the type coercion is local and obvious; reach for global pipes only when the rule applies uniformly to every request such as the ValidationPipe.

Pipes also have access to ArgumentMetadata, which includes the parameter type, the parameter name, and where the value came from (body, query, param, custom), useful when writing reusable pipes that need to behave differently depending on context. A common interview trick is asking when transformation happens relative to validation: the answer is that pipes run in registration order, so if you apply ValidationPipe before a transforming pipe the validator sees the raw string, not the coerced type.

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class TrimPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (typeof value !== 'string') return value;
    const trimmed = value.trim();
    if (!trimmed) throw new BadRequestException(`${metadata.data} cannot be blank`);
    return trimmed;
  }
}

Key Points

  • Pipes do transformation and validation between framework and handler
  • Built-ins: ParseIntPipe, ParseUUIDPipe, ParseBoolPipe, ValidationPipe
  • Apply per parameter, per handler, per controller, or globally
  • Pipes run in registration order; mind ordering when chaining
Q9

How do you handle errors and return correct status codes?

BasicError Handling

Answer

NestJS ships with a built-in HttpException class and a family of subclasses such as NotFoundException, UnauthorizedException, BadRequestException, ForbiddenException, ConflictException, UnprocessableEntityException and InternalServerErrorException. Throwing one of these from any controller, service, guard, or interceptor causes Nest to short-circuit the request and return a JSON response with the right status code and a structured payload. For domain-specific errors that should also map to HTTP responses, for example a PaymentDeclinedError or an InventoryUnavailableError, write a custom exception filter with @Catch(MyError) so callers can handle them by code, not by message.

For changing the default 200 status code on a handler, use @HttpCode(201). The single biggest beginner mistake is throwing raw Error objects: they fall through to Nest's generic 500 handler with a meaningless response body and no useful logging, which makes production debugging painful. Always throw a typed exception, even if it is a temporary one, your future self pulling on-call at 3 AM will thank you. A robust pattern in 2026 is to define one base AppError class with a code, message, statusCode, and optional cause field, then derive every domain error from it; a single ExceptionFilter then maps any AppError subtype to its JSON contract uniformly, while still letting you catch specific subclasses elsewhere.

import { Controller, Get, Param, NotFoundException, HttpCode } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get(':id')
  @HttpCode(200)
  findOne(@Param('id') id: string) {
    const user = this.users.find(id);
    if (!user) throw new NotFoundException(`User ${id} not found`);
    return user;
  }
}

Key Points

  • HttpException family for typed exceptions with correct status codes
  • Custom exception filters for domain errors like PaymentDeclined
  • Never throw raw Error, always use a typed subclass
  • A single AppError base + global filter scales well in large codebases
Q10

How do you configure environment variables in a Nest app?

BasicConfiguration

Answer

Use the official @nestjs/config package. It reads from process.env and from .env files (with dotenv under the hood), validates the schema with Joi or Zod, and exposes a ConfigService that you inject anywhere. Make ConfigModule.forRoot({ isGlobal: true }) the very first import of your AppModule so every other module can resolve ConfigService without re-importing.

Never read process.env directly inside services, that breaks both testability and validation. Always pass a validation schema so the application fails fast at boot if a required variable is missing or malformed: a DATABASE_URL with a typo should crash the container before serving a single request, not produce confusing 500s an hour later. In production, do not commit .env files; pull values from a real secret manager like AWS Secrets Manager, Doppler, HashiCorp Vault, or Infisical (popular among Indian devops teams).

For 12-factor compliance, every config value should be overridable by environment variable, even if it has a sensible default for local development. A useful refinement is the load function pattern, write small typed config factories per concern (database, jwt, redis) and register them via ConfigModule.forRoot({ load: [databaseConfig, jwtConfig] }). You then inject the typed config object directly rather than calling configService.get<string>('DATABASE_URL'), which gives you compile-time safety on every config access.

import { ConfigModule, ConfigService } from '@nestjs/config';
import * as Joi from 'joi';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
        DATABASE_URL: Joi.string().uri().required(),
        JWT_SECRET: Joi.string().min(32).required(),
      }),
    }),
  ],
})
export class AppModule {}

Key Points

  • @nestjs/config plus Joi or Zod for validated environment loading
  • Never read process.env directly inside services
  • Fail fast at boot if a required value is missing or malformed
  • Use typed load() factories for compile-time safety on config access
Q11

How do you enable CORS in a NestJS application?

BasicSecurity

Answer

NestJS wraps the standard cors package and exposes it as app.enableCors() in main.ts. You can pass it a configuration object specifying allowed origins, methods, headers, credentials, max age, and exposed headers. In production, never use { origin: true } or { origin: '*' } in combination with credentials: true, browsers reject that combination outright and it widens your attack surface anyway.

Whitelist your real front-end domains explicitly. For dynamic origins, such as multi-tenant SaaS where each tenant has a custom subdomain, pass a function that checks the incoming origin against a database or an env list and calls callback(null, true) when it matches. Pair CORS with helmet, rate limiting, and a strict Content-Security-Policy for a complete browser-security posture.

In India many SaaS firms also enforce origin checks via Cloudflare Access or Zero Trust in front of the Node service, so app-level CORS becomes a defence-in-depth layer rather than the only barrier. A common production bug is forgetting to expose custom response headers that the front-end needs to read, for example, an X-Total-Count pagination header is invisible to fetch() unless you list it in the exposedHeaders array, and engineers waste hours debugging seemingly missing data before realising the browser is silently filtering it out.

// main.ts
const app = await NestFactory.create(AppModule);
app.enableCors({
  origin: ['https://app.example.com', 'https://admin.example.com'],
  credentials: true,
  methods: ['GET', 'POST', 'PATCH', 'DELETE'],
  maxAge: 86400,
});
await app.listen(3000);

Key Points

  • Use app.enableCors() with explicit allowed origins in production
  • Never combine origin: '*' with credentials: true
  • Pair with helmet, rate limiting, and a Content-Security-Policy
  • Remember to list exposedHeaders for custom response headers
Q12

How does NestJS compare with plain Express in 2026?

BasicFundamentals

Answer

Express is a minimal, unopinionated HTTP library, you decide the project structure, error handling, validation, DI, and testing approach. NestJS uses Express as its default HTTP engine but wraps it with an Angular-style framework: modules, decorators, DI, pipes, guards, interceptors, exception filters, and an opinionated file layout. For very small services (a single webhook, a 200-line proxy), Express is faster to ship and produces a smaller bundle.

For anything with multiple resources, real authentication and authorisation, background jobs, and a team of more than two engineers, NestJS pays back its learning curve quickly, code stays organised, the DI container makes testing trivial, and new joiners can navigate the codebase the same way they would a Spring Boot or Angular app. In India many startups now default to NestJS over plain Express precisely because hiring people who can navigate a structured codebase is easier than maintaining a hand-rolled Express architecture, and because the framework's batteries-included approach (validation, error handling, OpenAPI generation, microservice transports) shortens the path from prototype to production. The performance overhead of NestJS over raw Express is small, typically two to five percent on synthetic benchmarks, and is recoverable many times over through architectural choices like FastifyAdapter and proper caching. Interviewers like this question because the right answer demonstrates that the candidate has thought beyond the framework's marketing copy and understands the trade-off between flexibility and structure.

Key Points

  • Express is unopinionated; NestJS layers structure on top of Express or Fastify
  • For trivial services Express is faster to ship; for non-trivial ones Nest wins
  • Nest's DI and testability scale better with team size
  • Performance overhead is small and easily recovered through Fastify and caching
Q13

What are guards and how are they different from middleware?

IntermediateAuthorization

Answer

A guard is a class annotated with @Injectable() that implements the CanActivate interface and returns a boolean (or a Promise/Observable of one) deciding whether the current request can proceed. Guards run AFTER middleware but BEFORE pipes and interceptors, and they have full access to the Nest execution context, so they can read controller and handler metadata via the Reflector. This makes them the right place for authentication and role-based authorization.

Middleware, by contrast, runs before the framework even resolves a route, so it does not know which controller will handle the request, useful for cross-cutting concerns like request logging or attaching a request ID, but a poor fit for auth that depends on route-level metadata like @Roles('admin'). The canonical pattern is a JwtAuthGuard that verifies the access token and attaches user info to the request, plus a RolesGuard that reads metadata set by a @Roles() decorator and matches it against the authenticated user's roles. Guards can also be combined, @UseGuards(JwtAuthGuard, RolesGuard), and they execute in order, short-circuiting on the first one that denies access.

For most production apps you also register a global JWT guard via APP_GUARD and then opt specific routes out with a @Public() decorator, which inverts the default to 'secure by default'. This is the pattern Razorpay-style fintech uses to avoid accidentally shipping an unauthenticated endpoint when an engineer forgets a guard.

import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(ctx: ExecutionContext): boolean {
    const required = this.reflector.get<string[]>('roles', ctx.getHandler());
    if (!required) return true;
    const { user } = ctx.switchToHttp().getRequest();
    if (!user || !required.some(r => user.roles.includes(r))) {
      throw new UnauthorizedException();
    }
    return true;
  }
}

Key Points

  • Guards implement CanActivate and return boolean / Promise<boolean>
  • Run after middleware, before pipes and interceptors
  • Have access to handler metadata via the Reflector
  • Register globally with APP_GUARD and use @Public() to opt out
Q14

What is an interceptor and when should you use one?

IntermediateInterceptors

Answer

An interceptor is a class implementing the NestInterceptor interface that wraps around a handler, it can run code BEFORE the handler executes and AFTER the response is produced, transforming or extending behaviour on either side. Interceptors are RxJS-based, returning an Observable, which makes them ideal for logging request latency, transforming response payloads (wrapping every response in { data, meta }), caching with CacheInterceptor, applying timeouts, and serialisation (with ClassSerializerInterceptor + class-transformer to strip sensitive fields like password hashes from API responses). The key difference from middleware is access to the execution context, and from filters is the ability to modify successful responses, not just errors.

A common interview question is the order in which Nest pieces execute: middleware -> guards -> interceptors (pre) -> pipes -> handler -> interceptors (post) -> exception filters. Being able to recite that order in the right direction is the easiest way to demonstrate intermediate-level fluency in a NestJS interview. Interceptors are also where you typically integrate OpenTelemetry tracing for richer per-handler spans, because they sit on both sides of the handler boundary and can finalise the span in finalize() regardless of whether the handler succeeded or threw.

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, map } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
    const req = ctx.switchToHttp().getRequest();
    const start = Date.now();
    return next.handle().pipe(
      tap(() => console.log(`${req.method} ${req.url} ${Date.now() - start}ms`)),
      map(data => ({ data, meta: { requestId: req.id, timestamp: Date.now() } })),
    );
  }
}

Key Points

  • Wraps handlers on both sides; RxJS-based
  • Use for logging, response wrapping, caching, timeouts, serialisation
  • Execution order: middleware -> guard -> interceptor pre -> pipe -> handler -> interceptor post -> filter
  • Natural home for OpenTelemetry span management
Q15

Exception filters vs error-handling interceptors, what is the difference?

IntermediateError Handling

Answer

Both can react to errors, but they sit at different layers. An exception filter (annotated with @Catch()) only runs when a handler, or any provider it called, throws an exception. Its job is to map errors to HTTP responses: format the JSON shape, set the status code, log the failure, and ensure the response body is deterministic.

The Nest framework already ships a default one, so you only write your own to customise the response shape (e.g. always { error: { code, message, requestId } } for a uniform error contract across services) or to handle non-HttpException types like Prisma's PrismaClientKnownRequestError. Error-handling inside an interceptor is done with RxJS's catchError operator: it runs on the same observable as the success path, so it can retry, transform, or fall back to a default value. Practical rule: use a global exception filter to standardise error JSON across the whole API, and use interceptor-level catchError only when you want behaviour like retry-with-backoff for transient downstream failures or graceful degradation when a non-essential dependency is down.

The two are complementary, not competing. A useful pattern in production: pair a global AllExceptionsFilter that catches anything Nest does not already handle (returning { error, requestId, timestamp } and logging the full stack) with a per-feature RetryInterceptor that retries idempotent HTTP calls on transient 5xx errors. This gives you clean error contracts at the API boundary and graceful recovery from flaky internal dependencies, without conflating the two concerns.

import { Catch, ArgumentsHost, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';

@Catch(HttpException)
export class AppExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception.getStatus();
    response.status(status).json({
      error: { code: exception.name, message: exception.message },
      requestId: ctx.getRequest().id,
      timestamp: new Date().toISOString(),
    });
  }
}

Key Points

  • Filters map exceptions to HTTP responses; interceptors can also retry or fall back
  • One global filter for consistent error contracts is the canonical pattern
  • catchError in interceptors is for retry / degradation, not response shaping
  • Pair AllExceptionsFilter (response shape) with RetryInterceptor (resilience)
Q16

What are the injection scopes in NestJS and when do you use REQUEST scope?

IntermediateDependency Injection

Answer

Nest providers have three scopes: DEFAULT (singleton, one instance for the whole application lifetime), REQUEST (a new instance for every incoming request), and TRANSIENT (a new instance everywhere it is injected). Default singleton is correct 95% of the time and the most performant. REQUEST scope is needed when a provider must read per-request state, for example, a logger that tags every log line with the request ID and authenticated user, or a tenant resolver in a multi-tenant SaaS that has to scope every query to the current tenant.

The trade-off is significant: any provider that depends on a REQUEST-scoped provider also becomes REQUEST-scoped, cascading up the dependency tree. That means Nest has to instantiate that whole sub-tree on every request, which is slower and prevents performance optimisations like Fastify's bypassing of certain hooks. The recommended alternative in 2026 is AsyncLocalStorage from node:async_hooks (via the @nestjs/cls package), which carries per-request context through singletons without forcing scope changes.

Even teams at Razorpay and Zerodha have migrated away from REQUEST scope toward AsyncLocalStorage for exactly this reason. TRANSIENT scope is the most exotic, it creates a new instance everywhere the provider is injected, which is useful for stateful helpers like rate limiters or per-call caches but is rarely the right answer in application code. If an interviewer asks you when to use TRANSIENT, the honest reply is 'almost never' followed by explaining why singleton plus AsyncLocalStorage covers virtually every legitimate case.

import { Injectable, Scope } from '@nestjs/common';
import { ClsService } from 'nestjs-cls';

// Avoid this, propagates REQUEST scope up the tree
@Injectable({ scope: Scope.REQUEST })
export class TenantAwareService {}

// Prefer this, singleton + AsyncLocalStorage
@Injectable()
export class TenantAwareService {
  constructor(private readonly cls: ClsService) {}
  currentTenantId(): string {
    return this.cls.get('tenantId');
  }
}
💡 Pro Tip: If you make UsersService REQUEST-scoped, every controller and provider that injects it transitively becomes REQUEST-scoped too. Use AsyncLocalStorage for ambient per-request context instead.
Q17

How do you handle circular dependencies between modules or providers?

IntermediateDependency Injection

Answer

Circular dependencies happen when module A imports module B and B imports A, or when two providers inject each other through their constructors. Nest gives you a forwardRef() helper for both cases, wrap the imports and inject sites with forwardRef(() => OtherModule) and the container resolves them lazily. However, this is almost always a code-smell, not a real solution.

A circular dependency means you have not identified a true layer boundary: extract the shared types or shared logic into a third module that both A and B depend on. Common refactors include pulling shared interfaces and value objects into a CommonModule, introducing an event emitter (EventEmitter2 or the @nestjs/event-emitter package) so the two services communicate via events without direct injection, or merging the two providers if they are really part of the same concept. The Nest bootstrap process warns at startup if it detects a forwardRef cycle, which is a strong signal to refactor rather than to silently ship.

Teams that allow forwardRef to proliferate end up with codebases that are very hard to test because every cycle has to be mocked twice. A useful heuristic during code review: if the proposed fix is to add forwardRef, push back and ask whether the two services should actually be one, or whether a domain event would break the cycle naturally. The forwardRef should be the answer of last resort, used only when a refactor would require changes to too many call sites in a single PR.

// Last-resort fix; prefer refactoring the cycle away.
import { Module, forwardRef } from '@nestjs/common';

@Module({
  imports: [forwardRef(() => B_Module)],
  providers: [A_Service],
  exports: [A_Service],
})
export class A_Module {}

// Inside A_Service constructor:
// constructor(@Inject(forwardRef(() => B_Service)) private readonly b: B_Service) {}

Key Points

  • forwardRef() resolves cycles lazily but is a code smell
  • Prefer extracting shared logic to a third module
  • Use domain events to decouple services that need to call each other
  • Reach for forwardRef only when a refactor is genuinely too large for one PR
Q18

How do you write unit tests for a NestJS service with mocked providers?

IntermediateTesting

Answer

NestJS ships with @nestjs/testing, which provides Test.createTestingModule(), a DI container builder that mirrors your real module configuration but lets you swap providers for mocks. The canonical pattern is to import the real service under test, then for each of its dependencies provide { provide: RealClass, useValue: mockObject } where mockObject is a jest-style stub. Compile the module and resolve the service via module.get().

From there it is standard Jest, call methods, assert on return values, assert on mock call arguments. This pattern is fast (no HTTP, no database, no Redis) and is what 80% of your test suite should be, because slow tests destroy iteration speed in CI. For mocking strategy, prefer jest.fn() over hand-written stubs because it gives you mockResolvedValue, mockRejectedValue, and the toHaveBeenCalledWith matcher.

Avoid over-mocking, if your test mocks every single dependency, you are testing the mocks, not the code. Lean on Test.createTestingModule({ imports: [RealUtilityModule] }) to wire in genuinely deterministic helpers. For more complex scenarios, use module.overrideProvider(SomeService).useValue(mock) on an already-compiled module, which is handy when you want to share a base test module across multiple test files but swap one specific provider per file. Senior interviewers usually check that you understand the difference between unit tests with mocks (fast, focused) and integration tests with real dependencies (slower, broader), and that you have a clear story for which goes where.

import { Test } from '@nestjs/testing';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';

describe('UsersService', () => {
  let service: UsersService;
  const repo = { findById: jest.fn(), save: jest.fn() };

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: UsersRepository, useValue: repo },
      ],
    }).compile();
    service = module.get(UsersService);
  });

  afterEach(() => jest.resetAllMocks());

  it('returns user when found', async () => {
    repo.findById.mockResolvedValue({ id: 1, name: 'Saksham' });
    expect(await service.get(1)).toEqual({ id: 1, name: 'Saksham' });
    expect(repo.findById).toHaveBeenCalledWith(1);
  });
});

Key Points

  • Test.createTestingModule() lets you swap providers for mocks
  • Use jest.fn() with mockResolvedValue / mockRejectedValue helpers
  • Override providers per-file with overrideProvider().useValue()
  • Reserve real dependencies for integration tests, not unit tests
Q19

How do you write end-to-end tests with Supertest in NestJS?

IntermediateTesting

Answer

For end-to-end tests, you boot the entire Nest application against an in-memory HTTP server via supertest. The standard recipe lives in test/app.e2e-spec.ts: build the testing module from AppModule (or a slimmed-down test variant), call createNestApplication(), apply the same global pipes and filters as production, then await app.init(). Pass app.getHttpServer() into supertest's request() function to fire real HTTP calls against the in-process server with zero network overhead.

Override database providers with an in-memory or test container so each test starts from a clean state, testcontainers-node is widely used in 2026 for spinning up real Postgres, Redis, or RabbitMQ instances during CI without polluting shared dev databases. Always call app.close() in afterAll() to release ports and clean up open handles, otherwise Jest hangs. A good rule is to keep your e2e tests focused on happy paths and a few critical failure cases, push exhaustive permutations of inputs into unit tests where they run in milliseconds, not seconds.

For authenticated routes, write a helper that mints a valid JWT in the test setup and attaches it via .set('Authorization', `Bearer ${token}`); never mock JwtAuthGuard out of the e2e suite, because then you are no longer testing the real auth pipeline. Contract testing with pact or schema snapshots is a useful third layer between unit and e2e for verifying that public API responses do not regress accidentally.

import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';

describe('Users e2e', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const module = await Test.createTestingModule({ imports: [AppModule] }).compile();
    app = module.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
    await app.init();
  });

  it('POST /users creates a user', () =>
    request(app.getHttpServer())
      .post('/users')
      .send({ email: 'a@b.c', age: 30 })
      .expect(201)
      .expect(({ body }) => expect(body.email).toBe('a@b.c')));

  afterAll(() => app.close());
});

Key Points

  • createNestApplication() + app.init() + supertest for in-process HTTP testing
  • testcontainers-node spins up real Postgres / Redis for hermetic CI
  • Mint real JWTs in setup instead of mocking auth guards
  • Keep e2e focused; push exhaustive cases down to unit tests
Q20

How do you implement microservices with NestJS over TCP, Redis, RabbitMQ or Kafka?

IntermediateMicroservices

Answer

NestJS has first-class support for microservices via the @nestjs/microservices package. Instead of NestFactory.create() you call NestFactory.createMicroservice() with a transport option, Transport.TCP, Transport.REDIS, Transport.RMQ, Transport.KAFKA, Transport.NATS, Transport.MQTT or Transport.GRPC. Controllers replace @Get with @MessagePattern (for request/response, RPC-style) or @EventPattern (for fire-and-forget, pub/sub-style).

On the client side, inject a ClientProxy configured for the same transport and call client.send() or client.emit(). Each transport has trade-offs: TCP is simple but offers no durability and is mostly used for local Docker-Compose setups, Redis Pub/Sub is fast but at-most-once with no persistence, RabbitMQ and Kafka provide durability and at-least-once delivery (Kafka also has ordered partitioning and long retention so you can replay history), NATS is ultra-low-latency and now also supports JetStream for durability, and gRPC gives you a strongly typed contract via Protobuf with end-to-end TypeScript generation. In Indian fintech and SaaS, RabbitMQ tends to win for synchronous-style RPC patterns and per-message acknowledgement requirements, and Kafka wins for high-throughput event streams and analytics pipelines.

A critical point interviewers probe is delivery semantics, for example, RabbitMQ at-least-once delivery means handlers must be idempotent (use a deduplication key or upsert pattern). Kafka consumers must also handle the rebalance event correctly when scaling up or down, otherwise you can lose messages or duplicate work.

import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
    transport: Transport.RMQ,
    options: { urls: ['amqp://rabbit:5672'], queue: 'orders_queue', queueOptions: { durable: true } },
  });
  await app.listen();
}

// In a controller:
import { MessagePattern } from '@nestjs/microservices';
@MessagePattern({ cmd: 'order.create' })
createOrder(data: CreateOrderDto) { return this.orders.create(data); }

Key Points

  • createMicroservice() with Transport enum; @MessagePattern / @EventPattern controllers
  • TCP, Redis, RabbitMQ, Kafka, NATS, MQTT, gRPC all supported
  • Each transport has different durability and ordering guarantees
  • At-least-once delivery means handlers must be idempotent
Q21

What are hybrid applications in NestJS?

IntermediateMicroservices

Answer

A hybrid application exposes both an HTTP server and one or more microservice transports from the same Nest process. This is the canonical pattern for services that need to receive synchronous HTTP from clients AND consume Kafka events or RabbitMQ messages, for example, an Orders service that handles POST /orders from the storefront AND listens for payment.captured events from a payment gateway. You build it by calling NestFactory.create() to get the HTTP app, then app.connectMicroservice() one or more times for each transport, then app.startAllMicroservices() before app.listen().

All controllers share the same DI container, so a single OrdersService can be called from both a @Post() HTTP handler and a @MessagePattern() handler without any duplication of logic. The trade-off is operational: scaling rules for HTTP traffic and message-queue throughput often differ, so for very high load you eventually split them into separate deployments where one set of pods serves HTTP and a different set runs the Kafka consumer. Until you hit that scale, hybrid keeps your deploy story simple, one image, one Helm chart, one health check. A practical caveat: health checks for hybrid apps must verify both the HTTP server and the message-broker connection, otherwise Kubernetes can keep routing traffic to a pod whose Kafka consumer has silently disconnected and whose event handlers are no longer running.

const app = await NestFactory.create(AppModule);
app.connectMicroservice<MicroserviceOptions>({
  transport: Transport.KAFKA,
  options: { client: { brokers: ['kafka:9092'] }, consumer: { groupId: 'orders-consumer' } },
});
await app.startAllMicroservices();
await app.listen(3000);

Key Points

  • HTTP + microservice transports in one process via connectMicroservice()
  • Shared DI container means single service for both entry points
  • Eventually split when scaling profiles diverge
  • Health checks must cover both HTTP and broker connectivity
Q22

How do you build a GraphQL API with NestJS using the code-first approach?

IntermediateGraphQL

Answer

Install @nestjs/graphql plus a driver, typically @nestjs/apollo with apollo-server-express in 2026, or @nestjs/mercurius if you are running Fastify. In code-first mode, you write TypeScript classes annotated with @ObjectType(), @Field(), @InputType(), @Args(), and Nest generates the GraphQL SDL at startup. This is preferred for most teams because the source of truth is TypeScript: refactors are checked by the compiler, IDE autocomplete works end-to-end, and you can co-locate types with resolver logic.

Schema-first works the other way around, you write a .graphql file and Nest generates types. Resolvers are NestJS providers annotated with @Resolver(), which means they get DI, guards, interceptors and pipes for free, exactly the same patterns you already use in REST controllers. For production: enable persisted queries to mitigate query-cost attacks where a client crafts an exponentially expensive query, set a query complexity limit using graphql-query-complexity, and disable GraphQL Playground in production unless you put it behind auth.

Use DataLoader to batch and de-duplicate per-request database calls, otherwise the classic N+1 problem will torch your DB under any real traffic. Field resolvers (annotated with @ResolveField()) let you compute associations lazily, a Post.author resolver only runs when the client actually asks for the author field, which is the secret to making GraphQL endpoints feel as fast as REST while remaining flexible.

import { Resolver, Query, Args, ObjectType, Field, Int } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => Int) id: number;
  @Field() email: string;
}

@Resolver(() => User)
export class UsersResolver {
  constructor(private readonly users: UsersService) {}

  @Query(() => User, { nullable: true })
  user(@Args('id', { type: () => Int }) id: number) {
    return this.users.findById(id);
  }
}

Key Points

  • Code-first uses TypeScript classes plus decorators to generate SDL
  • Resolvers are providers; they get guards, interceptors, pipes
  • Always pair with DataLoader to avoid N+1 queries
  • Set query complexity limits and disable Playground in production
Q23

Code-first vs schema-first GraphQL in NestJS, which should you pick?

IntermediateGraphQL

Answer

Code-first generates the SDL from TypeScript classes and decorators; schema-first generates TypeScript types from a hand-written .graphql file. Code-first wins when your backend is the source of truth, refactors are caught by the compiler, IDE autocomplete works end-to-end, and you can co-locate types with resolver logic. This is the dominant choice in 2026, especially in teams that already trust their TypeScript codebase and value end-to-end type safety from database to client.

Schema-first wins when the schema is owned by a separate team (mobile clients, partner integrations, external API consumers) and reviewed in pull requests as an explicit contract, the .graphql file becomes the API spec that survives backend rewrites. Schema-first also makes it slightly easier to share schemas across federated services via Apollo Federation. Pragmatically: if you are building one service and one team owns it, pick code-first; if you are designing a federated platform where multiple teams agree on a schema in advance, or your backend is the implementation detail of a public-facing API, pick schema-first.

Either way, you can change your mind later, Nest supports both styles in the same project, though most teams find the cognitive cost of mixing is not worth the flexibility. A practical tip is to write your code-first schema-generation step into CI so every PR produces a diffable SDL file, even if the SDL itself is generated, this gives you the contract-review benefits of schema-first without the duplication.

Key Points

  • Code-first: TypeScript is the source of truth, refactors compile-checked
  • Schema-first: .graphql file is the contract, easier for federation
  • In 2026 most single-team services pick code-first
  • Generate SDL in CI even for code-first to enable schema reviews
Q24

How do you integrate TypeORM or Prisma with NestJS?

IntermediateDatabase

Answer

For TypeORM, import @nestjs/typeorm and call TypeOrmModule.forRoot() in your root module with connection details (or forRootAsync() so config comes from ConfigService). Each feature module then calls TypeOrmModule.forFeature([Entity]) to expose entity repositories, which you inject with @InjectRepository(). For Prisma, there is no official @nestjs/prisma package, the standard pattern is a PrismaService that extends PrismaClient and connects in onModuleInit(); inject it everywhere a service needs database access.

Prisma has overtaken TypeORM in many 2026 codebases because the schema-first generation, excellent IDE support, predictable query API, and built-in migration tooling tend to win on developer experience. TypeORM still has more flexibility for very dynamic queries and complex inheritance hierarchies. Either way: always use migrations (TypeORM migrations or prisma migrate), never use synchronize: true in production (it will silently drop columns and destroy data), and pool connections via the database driver, PgBouncer in transaction mode if you have many workers behind a single Postgres instance, which is the norm at Indian fintech and SaaS scale.

For transactions, both libraries support a unit-of-work pattern: TypeORM gives you queryRunner.startTransaction() or @Transactional() decorators, and Prisma provides $transaction([...]) for sequential operations or $transaction(async tx => { ... }) for interactive transactions. Pick the pattern that matches your code style and stay consistent; mixing transaction approaches inside one service is a debugging nightmare.

// PrismaService pattern
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }
}

// Usage in a service:
@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}
  findOne(id: number) {
    return this.prisma.user.findUnique({ where: { id } });
  }
}

Key Points

  • @nestjs/typeorm + forRoot / forFeature for TypeORM, PrismaService extends PrismaClient for Prisma
  • Always use migrations; never synchronize: true in production
  • Pool connections at both Node and PgBouncer layers
  • Use unit-of-work transactions consistently across the codebase
Q25

How do you compare NestJS with Spring Boot for backend development?

IntermediateComparison

Answer

NestJS and Spring Boot share the same philosophical backbone: opinionated, modular, dependency-injected enterprise frameworks. Both lean heavily on decorators (or annotations in Java), both have first-class support for REST, GraphQL, microservices and messaging, and both prize testability above almost everything else. The differences are language-level: Spring Boot runs on the JVM, gives you mature concurrency primitives, garbage-collected throughput tuning, and a vast Java/Kotlin ecosystem; NestJS runs on Node.js with a single-threaded event loop, npm's enormous package ecosystem, and end-to-end TypeScript from frontend to backend.

For CPU-bound workloads (heavy data processing, BigDecimal financial math, large in-memory caches, complex graph traversal) Spring Boot generally wins because the JVM excels at this kind of work. For I/O-bound APIs (auth, CRUD, calling other HTTP services, websockets, event-driven message processing) the gap is small and NestJS often ships faster because frontend and backend engineers share a language and an editor. In Indian hiring, Spring Boot still dominates banks and large enterprises while NestJS is the go-to for startups and modern fintech, but the conceptual jump between them is small enough that hiring managers ask deep DI and architecture questions in either framework and expect senior candidates to translate the answers from one to the other.

Cold-start time is another practical difference: a Spring Boot app can take ten to thirty seconds to boot, while a NestJS app starts in under two seconds, which matters in autoscaling and serverless deployments. Memory footprint also tilts in Node's favour for I/O-bound services, a comparable NestJS pod will typically use 150-300 MB where a Spring Boot pod uses 500-800 MB, which translates directly into cluster bills at scale.

Key Points

  • Same architectural philosophy: opinionated, modular, DI-driven
  • Spring Boot wins for CPU-bound workloads on the JVM
  • NestJS wins for I/O-bound APIs and shared-language full-stack teams
  • NestJS has faster cold starts and lower memory; Spring Boot has more mature concurrency
Q26

How do you implement the CQRS pattern in NestJS?

AdvancedArchitecture

Answer

NestJS ships an official @nestjs/cqrs package that implements Command Query Responsibility Segregation on top of its DI container. The pattern splits writes (commands that change state) from reads (queries that return data) and adds an event bus that decouples side effects from the original action. You define plain TypeScript classes for CreateOrderCommand and OrderCreatedEvent, then write CommandHandlers and EventHandlers as @Injectable() providers.

Inject CommandBus into a controller and call commandBus.execute(new CreateOrderCommand(payload)), the matching handler executes, emits an OrderCreatedEvent on the event bus, and any number of event handlers (notification sender, analytics tracker, audit log writer, search index updater) react in parallel without the original command handler knowing about them. The pattern shines when an action triggers many side effects: each side effect is an EventHandler, easily added and removed without touching the original handler or any callers. It also lays the groundwork for event sourcing, where the event stream itself becomes the source of truth and read models are projections computed by replaying the events. Used pragmatically in Indian SaaS at firms like Razorpay and CRED for billing and order workflows where audit, idempotency and replay demands justify the extra architectural ceremony.

// Command
export class CreateOrderCommand {
  constructor(public readonly userId: string, public readonly items: Item[]) {}
}

// Handler
import { CommandHandler, ICommandHandler, EventBus } from '@nestjs/cqrs';
@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
  constructor(private readonly events: EventBus, private repo: OrdersRepository) {}
  async execute(cmd: CreateOrderCommand) {
    const order = await this.repo.create(cmd);
    this.events.publish(new OrderCreatedEvent(order.id));
    return order;
  }
}

// Event handler, runs independently, may be in another module
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
@EventsHandler(OrderCreatedEvent)
export class SendOrderEmailHandler implements IEventHandler<OrderCreatedEvent> {
  constructor(private mailer: MailerService) {}
  handle(event: OrderCreatedEvent) {
    return this.mailer.send('order-created', event.orderId);
  }
}
Q27

How do you build a custom decorator (including parameter decorators)?

AdvancedDecorators

Answer

NestJS decorators are thin wrappers around the standard TypeScript metadata reflection API. There are three useful kinds. Custom method or class decorators usually attach metadata via SetMetadata('key', value); pair them with a Reflector inside a guard to read the metadata at request time.

Custom parameter decorators use createParamDecorator((data, ctx) => ...) and return a value extracted from the execution context; the canonical example is a @CurrentUser() decorator that returns request.user, replacing repetitive @Req() req then req.user dereferences in every protected route. Composed decorators use applyDecorators() to bundle several decorators behind one, for example, an @Auth(...roles) decorator that combines @SetMetadata('roles', roles), @UseGuards(JwtAuthGuard, RolesGuard), and Swagger metadata like @ApiBearerAuth() in a single annotation. Custom decorators are how senior NestJS codebases stay readable: instead of 5 lines of decorator boilerplate on every protected route, you write @Auth('admin') and everyone on the team immediately understands the contract. They are also a strong signal in code reviews, a codebase with well-chosen custom decorators is usually maintained by engineers who think carefully about cross-cutting concerns.

import { createParamDecorator, ExecutionContext, applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';
import { ApiBearerAuth } from '@nestjs/swagger';

export const CurrentUser = createParamDecorator(
  (_: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user,
);

export function Auth(...roles: string[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(JwtAuthGuard, RolesGuard),
    ApiBearerAuth(),
  );
}

// Usage:
@Auth('admin')
@Get('me')
me(@CurrentUser() user: User) { return user; }
Q28

What are dynamic modules and when do you need them?

AdvancedModules

Answer

A dynamic module is a module whose contents are determined at runtime via static methods like forRoot() or forFeature(). They are the right tool whenever a module needs configuration before it can register its providers, think DatabaseModule.forRoot({ host, password }) or JwtModule.forRoot({ secret, expiresIn }). The pattern: the module class exposes a static method that returns a DynamicModule object, { module: ThisModule, providers: [...], exports: [...], imports: [...], global?: true }.

For configuration that itself depends on other providers (read JWT secret from ConfigService at runtime), expose a forRootAsync() variant that accepts useFactory: (config: ConfigService) => ({ ... }), inject: [ConfigService]. Almost every NestJS ecosystem package, @nestjs/typeorm, @nestjs/jwt, @nestjs/mongoose, @nestjs/config, @nestjs/throttler, @nestjs/bull, uses this pattern, and understanding it is what lets you write reusable internal modules that other teams in your organisation can adopt without copy-pasting. When you write a tenant module, a feature-flag module, or a wrapper around an external SDK, the dynamic module pattern is how you make it configurable, testable, and idiomatic.

// Dynamic module pattern
import { Module, DynamicModule } from '@nestjs/common';

@Module({})
export class FeatureFlagModule {
  static forRoot(options: { provider: 'launchdarkly' | 'flagsmith'; apiKey: string }): DynamicModule {
    return {
      module: FeatureFlagModule,
      providers: [
        { provide: 'FF_OPTIONS', useValue: options },
        FeatureFlagService,
      ],
      exports: [FeatureFlagService],
      global: true,
    };
  }

  static forRootAsync(options: { useFactory: (...args: any[]) => any; inject: any[] }): DynamicModule {
    return {
      module: FeatureFlagModule,
      providers: [
        { provide: 'FF_OPTIONS', useFactory: options.useFactory, inject: options.inject },
        FeatureFlagService,
      ],
      exports: [FeatureFlagService],
      global: true,
    };
  }
}
Q29

How do you instrument a NestJS app with OpenTelemetry for distributed tracing?

AdvancedObservability

Answer

OpenTelemetry is the de-facto standard for distributed tracing in 2026. The simplest setup: install @opentelemetry/sdk-node plus the auto-instrumentations bundle (@opentelemetry/auto-instrumentations-node), which patches Express, http, pg, ioredis, kafkajs, amqplib, mongodb, and many more without code changes. Start the SDK before NestFactory.create(), typically in a tracing.ts file imported at the very top of main.ts so instrumentation hooks load before any application module.

For richer per-handler spans, write a global interceptor that starts a child span around every handler, tags it with the route path, controller name, user id, and tenant id, then ends the span in the finalize() operator of the RxJS pipe. Export to an OTLP-compatible backend, Jaeger, Tempo, Honeycomb, Datadog, or SigNoz, which is increasingly popular among Indian teams running their own observability stack on a Kubernetes cluster. Trace context (W3C traceparent headers) propagates automatically through axios and http calls, so you get end-to-end traces across NestJS microservices and external APIs out of the box. Pair traces with structured logs (every log line includes the current traceId and spanId) and metrics (Prometheus exporter from the same SDK) for a complete three-pillar observability stack.

// tracing.ts, imported FIRST in main.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

const sdk = new NodeSDK({
  serviceName: 'orders-api',
  traceExporter: new OTLPTraceExporter({ url: process.env.OTLP_ENDPOINT }),
  instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();

// main.ts
import './tracing';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
Q30

How would you architect a NestJS backend for 10,000+ requests per second?

AdvancedArchitecture

Answer

Ten thousand requests per second from a single NestJS service is achievable in 2026 but requires deliberate choices at every layer. Stack: switch the HTTP adapter from Express to Fastify (FastifyAdapter) for a 15-30% throughput bump on JSON-heavy workloads. Run multiple Node processes per host with PM2 or Kubernetes, Node is single-threaded so vertical scaling means more processes, not bigger ones.

Behind a load balancer (nginx, ALB, or an envoy mesh) across 4-8 instances. The database is almost always the real bottleneck, use PgBouncer in transaction-pooling mode in front of Postgres, dedicated read replicas for query traffic, and aggressive cache-aside in Redis for hot reads. Push expensive work off the request path: BullMQ jobs on Redis for emails, image processing, ML inference, and webhook fan-out, never block a handler on it.

Avoid REQUEST-scoped providers everywhere, they force per-request instantiation and ruin DI performance; use AsyncLocalStorage for ambient context instead. Add per-handler timeouts with a global TimeoutInterceptor, better to fail fast than to queue requests until the upstream load balancer health-checks the pod out of rotation. Stream large responses (StreamableFile, NDJSON, SSE) instead of buffering them in memory.

Profile production with clinic.js or 0x; the usual culprits are sync JSON serialisation on huge payloads (switch to fast-json-stringify with pre-compiled schemas for known shapes) and over-eager logging at INFO level. Observability is non-negotiable: OpenTelemetry traces, p95/p99 latency SLOs, structured logs with request IDs, and synthetic checks against critical endpoints. Razorpay, Swiggy and Zerodha-style fintech backends in India run NestJS services in roughly this shape at scale, and senior interviewers from those companies will probe each of these layers in turn.

Key Points

  • Switch to FastifyAdapter for 15-30% throughput gain
  • Multiple processes per host (PM2 or k8s) and horizontal load balancing
  • PgBouncer + read replicas + Redis cache-aside
  • BullMQ for any background work; never block handlers
  • Avoid REQUEST scope; use AsyncLocalStorage instead
  • OpenTelemetry traces and p95/p99 SLOs from day one
  • Stream large responses; pre-compile JSON schemas for hot paths

Companies Hiring NestJS

Adidas
Roche
GitLab
Decathlon
Razorpay
Swiggy
Zerodha

Salary Insights

Average in India
₹8-25 LPA

Frequently Asked Questions

Is NestJS better than plain Express in 2026?

For non-trivial APIs, almost always yes. NestJS gives you dependency injection, modular structure, validation, guards, interceptors, microservice transports, and GraphQL out of the box, features you would otherwise bolt onto Express piecemeal with disparate libraries that do not agree on conventions. Plain Express is still a good choice for very small services or single-file webhooks where the structure would be overhead, but for anything intended to live in production for more than a quarter the NestJS investment pays off quickly.

How much does a NestJS developer earn in India?

Around ₹8-25 LPA in 2026 for mid-to-senior backend developers with NestJS as their primary stack. Razorpay, Swiggy, Zerodha, CRED, Postman, and global SaaS teams hiring out of India (GitLab, Adidas, Roche) pay at the upper end, especially for engineers comfortable with microservices, GraphQL, CQRS, and observability. Engineers who also know infrastructure (Kubernetes, Terraform, distributed tracing) can push past that range for staff-level roles.

Should I learn Angular before NestJS?

No, but the experience helps. NestJS borrows modules, decorators, and dependency injection from Angular, so Angular developers feel at home immediately. You do not need to know Angular to learn NestJS, solid TypeScript and an understanding of constructor-based dependency injection are enough. Many NestJS engineers in India come from a React background and learn the framework in two to three weeks without ever writing Angular code.

How does NestJS compare with Spring Boot for enterprise backends?

Conceptually very similar, both are opinionated, DI-driven frameworks with strong type systems, decorators or annotations, and unified support for REST, GraphQL, and messaging. The choice usually comes down to language and ecosystem: Spring Boot if your team is Java or Kotlin and CPU-heavy workloads dominate; NestJS if you want a TypeScript end-to-end stack, faster iteration on I/O-bound APIs, and shared tooling between backend and frontend engineers.

When should I use Fastify instead of Express in NestJS?

Switch to FastifyAdapter when you are serving high-throughput JSON APIs and want 15-30% more requests per second on the same hardware. The main trade-off is the Express ecosystem, some Express middleware does not work transparently and you may need Fastify-native plugins. For lower-traffic apps the default Express adapter is fine, and migrating later is a small refactor confined to main.ts and any custom middleware.

Is NestJS production-ready for fintech and payment platforms?

Yes, and it is widely used for exactly that. Indian fintech players like Razorpay, Zerodha, and CRED run NestJS in production for serious workloads including payment orchestration, fraud detection, and ledger services. The framework's structured DI and explicit interfaces make audit, testing, and compliance easier than ad-hoc Express services, which is exactly what regulated industries need. Pair it with Postgres for the core ledger, Kafka for event distribution, and OpenTelemetry for end-to-end traceability and you have an architecture that survives RBI audits.

Introduction

NestJS has firmly established itself as the default enterprise framework for Node.js in 2026. Built on top of Express (or optionally Fastify) and inspired heavily by Angular, it brings dependency injection, modular architecture, decorators, and strong TypeScript typing to a runtime that historically suffered from spaghetti Express code. Teams that previously used Spring Boot in Java or .NET Core in C# tend to feel immediately at home, while frontend engineers who have shipped Angular applications find the mental model so familiar that they can move into backend work with very little ramp time.

If you are interviewing for a NestJS role in India today, expect deep questions on modules and providers, dependency injection scopes, exception filters versus interceptors, microservice transports (TCP, Redis, RabbitMQ, Kafka), GraphQL with code-first schemas, and testing patterns using Jest with mocked providers. Indian fintech companies like Razorpay, Zerodha and CRED, along with global SaaS players like GitLab and Adidas, rely on NestJS for serious production backends, interviewers at those companies push hard on architecture, observability, and how you handle real production failure modes.

This guide walks through the 30 most-asked NestJS interview questions in 2026, grouped by difficulty. Each answer includes the underlying concept, common production gotchas, and a TypeScript code example where it adds clarity. Read the basic section to consolidate fundamentals, then push into the intermediate and advanced sections for the topics that actually decide senior offers, CQRS, custom decorators, dynamic modules, OpenTelemetry tracing, and scaling beyond ten thousand requests per second.

Ready to practice NestJS interviews?

Don't just read, practice these NestJS questions live with an AI interviewer that asks follow-ups and scores your answers.

AI-powered practice
Instant feedback
Free to start
Start Free Mock Interview