How would you structure a large NestJS codebase?
By domain, with clear boundaries:
- One module per bounded context (
billing,catalog,orders), each exporting a small API (a service or a facade), never its repositories or entities. - Inside a module, separate layers: controllers and resolvers (transport), application services (use cases), domain logic with no framework imports, and infrastructure (ORM repositories, HTTP clients) behind interfaces with injection tokens.
- Cross-module side effects through events, so modules do not import each other in both directions.
- A Nest monorepo (
nest g app,nest g library) when several apps share libraries such as auth or the domain model.
- One module per bounded context (
What is CQRS, and how does
@nestjs/cqrsimplement it?CQRS (command query responsibility segregation) separates writes (commands that change state) from reads (queries that return data), so each side can have its own model and scale independently.
export class PlaceOrderCommand { constructor(public readonly userId: string, public readonly items: Item[]) {} } @CommandHandler(PlaceOrderCommand) export class PlaceOrderHandler implements ICommandHandler<PlaceOrderCommand> { constructor(private repo: OrdersRepository, private events: EventBus) {} async execute({ userId, items }: PlaceOrderCommand) { const order = await this.repo.create(userId, items); this.events.publish(new OrderPlacedEvent(order.id)); return order.id; } } // controller await this.commandBus.execute(new PlaceOrderCommand(userId, dto.items));QueryBusand@QueryHandlerdo the same for reads, and sagas react to events with new commands. Worth it in complex domains with event-driven flows; for CRUD it adds files and indirection with no payoff.How do you make a NestJS API faster?
Measure first, then work from the biggest cost, which is rarely the framework:
- Database: indexes, N+1 queries, selecting only needed columns, connection pool size.
- Caching: hot reads in Redis, HTTP caching headers.
- Avoid request-scoped providers on hot paths: they rebuild their dependency tree per request.
- Keep CPU-heavy work (image resizing, PDF generation) off the event loop: worker threads or a queue.
- Switch to the Fastify adapter if framework overhead shows in the profile.
- Use every core: several replicas behind a load balancer.
Compression and TLS usually belong in the proxy, not in Node. Profile with
--cpu-profor clinic, and track p95 latency, not averages.How do you shut down a NestJS service gracefully?
Call
app.enableShutdownHooks()so Nest listens forSIGTERMandSIGINT, then close resources in the shutdown hooks.@Injectable() export class QueueWorker implements OnApplicationShutdown { async onApplicationShutdown(signal?: string) { await this.worker.close(); // finish the current job, take no new ones } }The order is
onModuleDestroy,beforeApplicationShutdown, then the HTTP server closes, thenonApplicationShutdown. Since NestJS 12 the Express adapter drains in-flight requests on shutdown. In Kubernetes, also fail the readiness probe first, so the load balancer stops sending traffic before the pod stops, and keep total shutdown withinterminationGracePeriodSeconds.How do you secure a NestJS API?
A short checklist:
- Validate all input: global
ValidationPipewithwhitelistandforbidNonWhitelisted. - Authenticate with a global guard and opt routes out explicitly with
@Public(). - Authorize per resource, not only per role: check that the order belongs to the user.
- Rate limit with
@nestjs/throttler, stricter on login and signup. - Restrict CORS to known origins.
- Security headers and CSRF protection for cookie sessions.
- Never leak internals: let unknown errors become
500with a generic message.
NestJSimport { Body, Controller, Get, Module, Post } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; // GET /account @Controller('account') class AccountController { private email = 'ada@example.com'; @Get() show() { return { email: this.email }; } @Post('email') change(@Body() body: { email: string }) { this.email = body.email; return { email: this.email }; } } @Module({ controllers: [AccountController] }) class AppModule {} async function bootstrap() { const app = await NestFactory.create(AppModule); app.useSecurityHeaders(); app.enableCsrfProtection({ trustedOrigins: ['https://admin.example.com'] }); await app.listen(3000); } bootstrap();useSecurityHeaders()sends helmet's headers (check the Headers tab), new in NestJS 12.1. SendPOST /account/emailwith{"email": "x@y.z"}and the headerSec-Fetch-Site: cross-site:403. Withsame-originit succeeds.- Validate all input: global
How do you manage configuration across environments in NestJS?
Use
@nestjs/config: load environment variables once, validate them at startup, and inject typed config instead of readingprocess.envacross the code.const database = registerAs('database', () => ({ url: process.env.DATABASE_URL, poolSize: Number(process.env.DB_POOL ?? 10), })); @Module({ imports: [ConfigModule.forRoot({ isGlobal: true, load: [database] })] }) export class AppModule {} @Injectable() export class DbHealth { constructor(@Inject(database.KEY) private db: ConfigType<typeof database>) {} }Fail fast: a missing environment variable should stop startup, not produce
undefinedat 3 AM. Keep secrets in the platform's secret store, and the same build artifact for every environment, with only the environment differing.How should a NestJS service log in production?
As structured JSON to stdout, with a request id on every line, at
loglevel or above.The built-in
Loggercan do it:NestFactory.create(AppModule, { logger: new ConsoleLogger({ json: true }) }). In NestJS 12, plain objects passed after the message become structured fields (this.logger.log('order placed', { orderId })); setstructuredParams: falsefor the old output. Many teams usenestjs-pinoinstead for speed and automatic request logging.Rules: never log passwords, tokens or full card numbers; log the error with its stack once, where it is handled, not at every layer; and use
bufferLogs: truewithapp.useLogger()so startup logs go through the same logger.How do you build a multi-tenant NestJS API?
Resolve the tenant once per request (from a subdomain, a header or the JWT), make it available to the code that queries data, and make every query filter by it.
Options for carrying the tenant:
- Request-scoped providers: simple, but the whole chain is rebuilt per request.
- Durable providers: a
ContextIdStrategygroups requests by tenant, so request-scoped trees are cached per tenant instead of per request. AsyncLocalStorage(vianestjs-cls): no scope changes; a repository reads the tenant from the store.
For storage: a
tenant_idcolumn with PostgreSQL row-level security, a schema per tenant, or a database per tenant, trading cost for isolation.How do you design error handling across a NestJS application?
Keep HTTP out of the domain and translate at the edge:
- Services throw domain errors (
InsufficientStockError) with no status codes. - One global filter maps domain errors to HTTP (
409,422) with a stable body and a machine-readable code. - Anything unknown becomes a
500with a generic message, logged once with the stack and the request id.
@Catch(DomainError) export class DomainErrorFilter implements ExceptionFilter { catch(err: DomainError, host: ArgumentsHost) { host.switchToHttp().getResponse().status(err.status).json({ statusCode: err.status, errorCode: err.code, message: err.message, }); } }The same services then work under GraphQL or a queue consumer, each with its own translation.
- Services throw domain errors (
How do you add health checks to a NestJS service?
@nestjs/terminusprovides aHealthCheckServiceand indicators for the database, memory, disk and HTTP dependencies.@Controller('health') export class HealthController { constructor( private health: HealthCheckService, private db: TypeOrmHealthIndicator, ) {} @Get() @HealthCheck() check() { return this.health.check([() => this.db.pingCheck('database', { timeout: 1500 })]); } }It answers
200when every check passes and503with details otherwise. Split liveness (the process responds) from readiness (dependencies are reachable): a database outage should take the pod out of the load balancer, not restart it in a loop.What should you check when upgrading a NestJS app to version 12?
- Node.js: 20.19+ or 22.12+ (no 21.x). The ES module packages rely on
require(esm). - CommonJS apps keep working and moving your own code to ES modules is optional. If you do move (CommonJS vs ES modules): relative imports need file extensions,
__dirnamebecomesimport.meta.dirname, and TypeScript path aliases need a resolver at runtime. - Lifecycle hooks now run by level in the module hierarchy; review code that relied on the old order between modules.
- Logger: objects after the message become structured params.
- Ecosystem:
@nestjs/configvalidation moved to Standard Schema; NATS v3 andgraphql-wsreplace older packages.
Run
nest upgrade --dry-run, upgrade every@nestjs/*package together, and run the e2e suite.- Node.js: 20.19+ or 22.12+ (no 21.x). The ES module packages rely on
Advanced NestJS Interview Questions
Architecture, CQRS, performance, security, configuration, logging and upgrading to NestJS 12.