Nest

Master the job description

Study Guide

Every requirement a senior NestJS / Node.js role commonly asks for — explained at senior depth. Read the concept, then the “how to say it” line so you can deliver a crisp, correct answer out loud.

How to use this Each topic explains the concept at the depth a senior NestJS / Node.js interview expects, then gives a “how to say it” line — the crisp sentence to deliver out loud. Read for understanding first; rehearse the one-liners last. Everything is current to NestJS 11 and Node.js 24 LTS.

01 · NestJS, TypeScript & the framework model

What they want: you understand why Nest exists. It's a progressive, opinionated Node.js framework written in TypeScript that sits on an HTTP adapter (Express by default, or Fastify) and adds an IoC/DI container, a module system, and decorators. It solves the architecture gap Express leaves open.

Nest combines OOP, FP, and FRP (RxJS in interceptors/microservices). It leans on TypeScript's emitted decorator metadata (reflect-metadata) to know a constructor parameter's type — which is the injection token. That's why tsconfig needs experimentalDecorators + emitDecoratorMetadata, and why DI works on classes, not interfaces.

How to say it “Nest is structure and dependency injection on top of Node's HTTP layer — Express with an opinionated architecture, written in TypeScript and modeled on Angular's module/provider system.”

02 · Modules & encapsulation

Core: a module (@Module) groups related controllers and providers; the app is a tree of modules rooted at AppModule. Providers are private to their module unless exported; exports is the module's public API and imports brings another module's exports in.

  • Every module is a singleton — export a provider once and all importers share one instance.
  • Listing the same class in two modules' providers creates two instances (a state-sharing bug).
  • @Global() exposes exports everywhere without importing — reserve it for cross-cutting infra (config, DB, logging); overusing it hides coupling.
How to say it “Modules give you encapsulation: a provider is private until I export it. I keep boundaries explicit and avoid @Global() except for true infrastructure.”

03 · Controllers & routing

Core: controllers (@Controller('users')) map routes to handlers (@Get(':id'), @Post()). Return a value and Nest serializes it to JSON (200, or 201 for POST). Read inputs with @Param, @Query, @Body, @Headers, often combined with a pipe (@Param('id', ParseIntPipe)).

Gotcha Injecting @Res() switches to Express-mode: you must send the response yourself and lose interceptors/@HttpCode. Use @Res({ passthrough: true }) if you only need to set a cookie/header.
NestJS 11 Express 5 changed wildcard routing — bare * must be named: @Get('files/*')@Get('files/*splat').
How to say it “Controllers are the thin HTTP layer — parse and delegate. The real work lives in injected services.”

04 · Providers & dependency injection

Core: a provider is anything injectable (@Injectable() services, repositories, factories, values). A consumer declares a dependency by type in its constructor; that type is the token. The module registers a provider for the token; at bootstrap the container builds the graph transitively and instantiates bottom-up, caching singletons.

Custom providers give you control:

ProviderUse
useClassResolve a token to a class (swap by env).
useValueInject a constant / mock / external instance.
useFactoryBuild dynamically (async ok), inject deps via inject:[].
useExistingAlias a token to an existing one (same singleton).

Inject non-class deps (interfaces, config) via a string/symbol token + @Inject(TOKEN) — the ports-and-adapters mechanic.

How to say it “DI means classes declare what they need and the container provides it. I depend on interfaces via tokens so implementations are swappable and mockable.”

05 · Injection scopes & lifecycle

Core: three scopes — DEFAULT (singleton, recommended), REQUEST (per request), TRANSIENT (per consumer). Singletons are safe because Node isn't thread-per-request; only store request-specific state in REQUEST scope (or better, AsyncLocalStorage).

The cost of REQUEST It bubbles up — a request-scoped leaf makes its consumers (up to the controller) request-scoped, adding per-request allocation/GC. A shared DB/logger going request-scoped can convert the whole app. Use durable providers + a tenant ContextIdStrategy to recover performance.

Lifecycle hooks (order): onModuleInitonApplicationBootstrap → [running] → onModuleDestroybeforeApplicationShutdownonApplicationShutdown. Shutdown hooks fire only after app.enableShutdownHooks(); they don't run for request-scoped providers.

How to say it “I default to singletons and reach for request scope only for genuine per-request state — and I know it bubbles, so I prefer AsyncLocalStorage for context.”

06 · Dynamic modules & configuration

Core: a dynamic module returns its metadata from a static method so it can be configured. Convention: register() = per-importer, forRoot() = once app-wide, forFeature() = per-feature tweak of a forRoot. Each has an async variant (forRootAsync) taking useFactory+inject so options can come from ConfigService.

The modern implementation is ConfigurableModuleBuilder, which auto-generates the base class, the options token, and the sync/async signatures — removing hand-written boilerplate.

NestJS 11 Importing the same dynamic module twice with deeply-equal config now yields separate instances — assign const m = X.forRoot({...}) to a variable and reuse it to share one.
How to say it “forRoot configures a module once globally; forFeature tweaks it per feature; the async variants inject config. I use ConfigurableModuleBuilder so I'm not hand-writing forRootAsync.”

07 · The request lifecycle

Memorize this: Incoming request → MiddlewareGuardsInterceptors (pre)PipesHandler (→ Service) → Interceptors (post)Exception filters → Response. Within each level it's global → controller → route; interceptors unwind on the way out, and filters are the only enhancer that resolves route → controller → global.

BlockJob · has ExecutionContext?
MiddlewarePre-routing raw req/res — no context
GuardAuthorization — yes (Reflector)
InterceptorAOP before/after (RxJS) — yes
PipeValidate/transform args — metadata only
FilterShape errors — ArgumentsHost only
How to say it “Middleware, guards, interceptors, pipes, handler, interceptors again, then filters on error. Only guards and interceptors get the ExecutionContext.”

08 · Pipes & validation

Core: pipes (transform(value, metadata)) validate and transform handler args, running just before the handler inside the exceptions zone. The global ValidationPipe + class-validator on class DTOs is the backbone:

app.useGlobalPipes(new ValidationPipe({ whitelist: true, // strip undecorated props forbidNonWhitelisted: true, // throw on unknown props transform: true, // build DTO instance + coerce }));

whitelist/forbidNonWhitelisted defend against mass-assignment. Nested objects need @ValidateNested() + @Type(() => Dto). Built-in parse pipes: ParseIntPipe, ParseUUIDPipe, ParseArrayPipe, and v11's ParseDatePipe.

Trap DTOs must be classes; import type erases them and validation silently no-ops.
How to say it “A global ValidationPipe with whitelist + transform validates class DTOs and blocks mass-assignment — bad input becomes a 400 before my handler runs.”

09 · Guards & authorization

Core: a guard (canActivate(ctx) → boolean) decides whether a request proceeds — the home of authorization. Because it has an ExecutionContext, it reads route metadata via Reflector:

const roles = this.reflector.getAllAndOverride(ROLES_KEY, [ctx.getHandler(), ctx.getClass()]);

RBAC: @Roles('admin') + a RolesGuard comparing to req.user.roles. Global-auth pattern: register the JWT guard as APP_GUARD (everything protected), add @Public() and short-circuit on it. For per-resource rules step up to ABAC with CASL.

How to say it “Guards do authorization. I make routes protected-by-default via APP_GUARD with a @Public() escape hatch, and use the Reflector for role checks.”

10 · Interceptors

Core: interceptors wrap the handler in an RxJS stream (intercept(ctx, next) → Observable), giving before/after logic around next.handle(). Skip next.handle() to override (caching).

  • Transform responses — map(d => ({ data: d }))
  • Logging/timing — tap(...)
  • Timeouts — timeout(5000) + catchError
  • Serialization — ClassSerializerInterceptor (@Exclude/@Expose)
How to say it “Interceptors are AOP: I use them for response envelopes, timing, timeouts, caching, and serialization — anything that wraps the handler.”

11 · Exception filters & error handling

Core: throw HttpException subclasses (NotFoundException, BadRequestException) and Nest's built-in filter shapes the response; unknown errors → 500. A custom filter (@Catch() + catch(exception, host)) lets you standardize the error body, log, and map domain errors to HTTP.

AllExceptionsFilter pattern: @Catch() (everything) + inject HttpAdapterHost; use ArgumentsHost so it works across HTTP/WS/RPC. Extend BaseExceptionFilter and call super.catch() to keep defaults while adding logging.

How to say it “I throw typed HttpExceptions for expected failures and use a global AllExceptionsFilter to standardize the error envelope and centralize logging.”

12 · Middleware

Core: middleware runs first, with raw req/res/next — logging, CORS, helmet, body parsing, attaching a request id. Class middleware (NestMiddleware) is DI-capable; functional middleware has no deps. Apply via configure(consumer): consumer.apply(LoggerMiddleware).forRoutes('users').

Limitation Middleware has no ExecutionContext — it can't know the target handler or read its metadata. For anything route-aware, use a guard or interceptor. Global app.use() middleware can't use DI.
How to say it “Middleware is for cross-cutting request setup that doesn't need to know the handler — logging, helmet, request ids. Route-aware logic goes in guards/interceptors.”

13 · Configuration & secrets

Core: @nestjs/config loads .env + merges process.env (real env wins). Make it global, cache it, and validate at boot with Joi/zod so a misconfigured deploy crashes immediately instead of failing at request time. Namespace with registerAs('db', ...) for typed, modular config.

12-factor: config that varies per deploy lives in the environment, not the repo. .env is local-dev only (gitignored + dockerignored); prod secrets come from a manager (k8s Secrets, AWS Secrets Manager, Vault). Node 20+ can load env natively with --env-file.

How to say it “Config is validated at startup so we fail fast, secrets come from a manager not the repo, and I namespace config for type safety.”

14 · Databases & the repository pattern

Core: Nest integrates TypeORM, Prisma, and Mongoose. TypeORM: forRoot configures the DataSource, forFeature([Entity]) registers per module, inject @InjectRepository(User). Prisma: wrap the generated client in a PrismaService (connect in onModuleInit). Mongoose: @InjectModel over @Schema classes.

The repository pattern keeps the app talking to repositories, not the raw ORM/SQL — so you can swap the store, add caching, and map rows to domain models at the boundary. Don't leak entities (with secrets/relations) straight into API responses.

#1 gotcha TypeORM synchronize: true auto-alters the schema and can drop data — never in production. Use migrations.
How to say it “I go through repositories and map to domain models at the boundary, run migrations (never synchronize in prod), and keep entities out of the API contract.”

15 · Transactions & data integrity

Core: wrap multi-step writes in a transaction so they commit or roll back atomically. TypeORM gives three ways: a QueryRunner (most control, must release() in finally), the callback dataSource.transaction(async mgr => ...) (auto commit/rollback), or the community typeorm-transactional decorator. Every operation must share the same EntityManager or it runs outside the transaction.

Across services you can't use a DB transaction — use the Saga pattern (compensating actions) and the transactional outbox for reliable event publishing, with idempotent consumers.

How to say it “In-service, I use a transaction with a shared EntityManager. Across services there's no 2PC — I use sagas with compensations and an outbox for atomic event publishing.”

16 · Authentication (JWT / Passport)

Core: issue a short-lived access token on login (JwtService.signAsync, id in sub) and verify it in a guard on protected routes. With Passport, a JwtStrategy extracts and validates the token (return value → req.user) and you protect with AuthGuard('jwt'); without Passport, a hand-rolled guard uses JwtService directly.

Pair access tokens with refresh tokens (rotated, stored, revocable) since JWTs can't be revoked before expiry. Hash passwords with bcrypt/argon2 (never store plaintext or reversible).

How to say it “Short-lived JWT access tokens plus rotating refresh tokens, verified in a global guard with a @Public() opt-out, passwords hashed with bcrypt/argon2.”

17 · Caching

Core: @nestjs/cache-manager (v11 is Keyv-based) gives manual caching (@Inject(CACHE_MANAGER)get/set/del) and automatic GET caching (CacheInterceptor). Patterns: cache-aside (check → miss → DB → set TTL → invalidate on write), read-through, write-through. Name stale-while-revalidate for instant reads + background refresh.

At scale The default store is in-memory (per-instance). Use a Redis store (KeyvRedis) so all replicas share the cache, and scope keys (e.g. by tenant). Guard against cache stampede with a lock or jittered TTL. TTLs are in milliseconds; get() returns undefined on miss in v11.
How to say it “Cache-aside with Redis so it's shared across instances, TTL + delete-on-write invalidation, and stampede protection for hot keys.”

18 · Queues & background jobs

Core: move heavy/slow work off the request path with BullMQ (@nestjs/bullmq, Redis-backed). Producer adds jobs (q.add('name', data, { attempts, backoff, delay, priority })); a @Processor extends WorkerHost and routes by job.name in process(). You get retries, backoff, delays, priorities, and durable persistence.

Idempotency Delivery is at-least-once — a job can run twice (crash before ack). Make processors idempotent (dedupe key / upsert), set timeouts above p99, and route exhausted retries to a DLQ with alerting. In BullMQ, @Process('name') does not work — switch on job.name.
How to say it “CPU-bound or slow work goes to a BullMQ queue with retries and backoff; consumers are idempotent because delivery is at-least-once, and dead jobs land in a DLQ.”

19 · Task scheduling & events

Core: @nestjs/schedule gives @Cron(), @Interval(), @Timeout() and a SchedulerRegistry for dynamic jobs. @nestjs/event-emitter gives in-process pub/sub (emit / @OnEvent) for decoupling side effects from the main flow.

Multi-replica trap Cron runs per-instance — with N replicas every instance fires. Use a distributed lock or enqueue a single job so it runs once. The event emitter is synchronous and non-durable — for cross-service or reliable delivery use a queue/broker.
How to say it “In-process events for decoupling; a distributed lock or a queue for scheduled work so a cron doesn't fire on every replica.”

20 · Testing strategy

Core: @nestjs/testing builds a DI graph you can override. Unit: Test.createTestingModule({...}).overrideProvider(X).useValue(mock).compile(), then module.get() (or resolve() for scoped). E2E: createNestApplication()init() → drive with supertestapp.close().

The pyramid: many fast unit tests (mock collaborators) → fewer integration tests (real DB/Redis via Testcontainers) → a few e2e. Test the five outcomes of a flow: response, DB change, outgoing call, queued message, observability.

NestJS 12 Vitest is slated to replace Jest as the default runner — the patterns (createTestingModule, overrides, supertest) stay the same.
How to say it “Mostly fast unit tests with overridden providers, integration tests against real Postgres/Redis via Testcontainers, and a thin e2e layer with supertest.”

21 · The Node.js event loop

Core: Node runs your JS on one thread atop libuv. The loop's phases, in order: timers → pending callbacks → idle/prepare → poll (I/O) → check (setImmediate) → close callbacks. Between every callback, two microtask queues drain: process.nextTick first, then Promises.

Key facts: inside an I/O callback setImmediate beats setTimeout(0); recursive nextTick can starve the loop; the thread pool (default 4) serves fs, dns.lookup, crypto, zlib — but not network I/O (that's kernel-async).

How to say it “One JS thread, libuv phases timers→poll→check, microtasks drain between callbacks. Network I/O is kernel-async; fs/crypto/zlib use the 4-thread pool.”

22 · Async patterns & error handling

Core: callbacks → promises → async/await; most core APIs have promise variants (fs/promises, timers/promises). Know the combinators: all (all-or-fast-fail), allSettled (collect partials), race (first to settle), any (first success). Use AbortController/AbortSignal for cancellation and timeouts.

Pitfalls: floating promises (errors vanish), forEach with async (doesn't await), unbounded Promise.all over huge arrays (use a concurrency limiter), and unhandled rejections (process exits by default since Node 15). Distinguish operational errors (handle) from programmer errors (let it crash).

How to say it “async/await with try/catch, AbortSignal for cancellation, bounded concurrency for fan-out, and I crash-and-restart on programmer errors rather than swallowing them.”

23 · Streams & backpressure

Core: four types — Readable, Writable, Duplex, Transform. Stream large data instead of buffering it into memory. Backpressure stops a fast producer overrunning a slow consumer: write() returns false + the 'drain' event is the manual protocol.

pipeline > pipe Always use stream.pipeline(): pipe() doesn't forward errors or clean up on failure (FD leaks). Readables are async-iterable — for await (const chunk of stream) respects backpressure automatically.
await pipeline( fs.createReadStream(src), zlib.createGzip(), fs.createWriteStream(dst), );
How to say it “Stream large payloads with pipeline so backpressure and error cleanup are handled — never buffer a whole file in memory.”

24 · Concurrency: worker_threads, cluster, child_process

Core: Node scales by not blocking the loop and by using more processes/threads.

ToolUse
worker_threadsCPU-bound JS in-process; shared memory via SharedArrayBuffer.
clusterScale a stateless HTTP server across cores (processes sharing a port).
child_processRun external programs: spawn (stream), exec (buffer + shell), fork (Node + IPC).

In containers, N single-process replicas behind a load balancer often replace in-process cluster. Keep instances stateless (state in Redis/DB) so any replica serves any request.

How to say it “Scale I/O with replicas/cluster, offload CPU work to a worker-thread pool, shell out with child_process — and keep app instances stateless.”

25 · Modules: CommonJS vs ESM

Core: CJS (require/module.exports) is synchronous with free __dirname; ESM (import/export) is async, statically analyzable, supports top-level await, and uses import.meta.dirname. Opt in with "type": "module" or .mjs.

Interop: ESM imports CJS fine; CJS importing ESM was dynamic-import()-only until synchronous require(esm) stabilized in Node 24. The "exports" field defines public entry points and encapsulates internals (no deep imports). NestJS 12 plans a CJS→ESM transition.

How to say it “ESM is async/static with top-level await; CJS is sync. Node 24 made require(esm) work, and the exports field controls a package's public surface.”

26 · Performance & profiling

Core: the #1 rule is don't block the event loop — offload CPU work, avoid sync APIs on the hot path, and prefer native methods. Measure, don't guess: perf_hooks.monitorEventLoopDelay() for loop lag, --cpu-prof / clinic.js / 0x for flamegraphs, heap snapshots for memory.

Other levers: connection pooling, keep-alive, response compression (ideally at the proxy), caching, pagination + streaming for large results, and the Fastify adapter for raw throughput. Distributed state (rate limits, cache, sessions) must live in Redis so replicas agree.

How to say it “I profile with flamegraphs and event-loop-lag metrics, keep CPU work off the loop, pool connections, and cache — then verify with a load test.”

27 · Memory management & leak hunting

Core: V8 uses a generational GC — a fast Scavenger for the young generation (most objects die young) and Mark-Sweep-Compact for the old generation. A leak shows as steadily growing old-space retained size across snapshots.

Common causes: unbounded caches/Maps, forgotten event listeners (MaxListenersExceededWarning), timers never cleared, closures capturing large objects, module-level globals that only grow. Hunt by comparing two heap snapshots over time (retained constructors + retainer paths). WeakMap/WeakRef help where appropriate.

How to say it “Leaks are usually listeners, timers, or unbounded caches. I take two heap snapshots under load and diff retained objects to find the retainer path.”

28 · Microservices & transports

Core: a Nest microservice uses a non-HTTP transport (TCP, Redis, NATS, RabbitMQ, Kafka, gRPC) with the same DI/pipes/guards/filters. Request-response uses @MessagePattern + client.send() (cold Observable); events use @EventPattern + client.emit() (fire-and-forget). Throw RpcException; RPC filters return Observables.

Pick gRPC for typed low-latency RPC, Kafka for durable replayable streams, RabbitMQ for reliable work queues. An API gateway (often the HTTP half of a hybrid app) fronts the services and centralizes auth/routing/aggregation. Keep consistency with sagas + outbox + idempotency, not 2PC.

How to say it “Same Nest building blocks over a message transport; send() for RPC, emit() for events; gateway for cross-cutting concerns; sagas and outbox for consistency.”

29 · GraphQL

Core: prefer code-first (TS classes generate the SDL — no drift). Resolvers hold @Query/@Mutation/@ResolveField; field resolvers fetch relations lazily. The headline senior topic is the N+1 problem: a field resolver firing per parent over a list → 1+N queries. Fix with DataLoader batching (1+1) — return results in the same order/length as keys, and make the loader per-request.

Subscriptions use graphql-ws (back PubSub with Redis in prod). Federation splits the graph into subgraphs + a gateway via @key + @ResolveReference().

How to say it “Code-first resolvers, and I solve N+1 with per-request DataLoader that batches per-key loads into one IN query.”

30 · WebSockets & real-time

Core: gateways (@WebSocketGateway) handle @SubscribeMessage events with connection lifecycle hooks. socket.io (IoAdapter) gives rooms/namespaces/acks; ws (WsAdapter) is leaner. Gateways are singletons (can't be request-scoped) — keep per-connection state on the socket.

Scale across instances with the socket.io Redis adapter (pub/sub fans out room events) plus sticky sessions or websocket-only transport. Authenticate in the handshake, not per message. For real-time choices: WebSocket for two-way, SSE for one-way server push, push notifications when the app is closed.

How to say it “Gateways with socket.io + the Redis adapter to fan out across nodes, auth at the handshake, and SSE when I only need one-way push.”

31 · Security hardening

Core: the biggest real risk is the supply chain — lockfile + npm ci, audit/Snyk, minimize deps, --ignore-scripts. App-level: validate all input (whitelist DTOs), limit body size (single-thread DoS), rate-limit, helmet headers, strict CORS, parameterized queries (injection), guard against prototype pollution and ReDoS, hide error internals, run as non-root.

Permission model --permission is a seat belt for trusted code, not a sandbox against malicious code. Real isolation is OS-level (containers, seccomp).
How to say it “Lockfile + audits for supply chain, validation + parameterized queries + helmet + rate limiting at the app, and OS-level isolation for real sandboxing.”

32 · Logging & observability

Core: structured JSON logs (pino via nestjs-pino) to stdout, with redaction of secrets and a correlation id per request (via nestjs-cls/AsyncLocalStorage) so every line is traceable. Add OpenTelemetry tracing (load the SDK before the app) — traces are span trees sharing a trace id, propagated cross-service via the traceparent header.

Monitor four signals: uptime/health, metrics (incl. event-loop lag, RED), traces, and logs. Alert on guardrails (error rate, p99 latency, loop lag).

How to say it “JSON logs with correlation ids, OpenTelemetry traces correlated to logs, and alerts on event-loop lag and p99 — not just CPU.”

33 · CI/CD, Docker & deployment

Core: a multi-stage Dockerfile — build stage compiles + prunes dev deps, runtime stage copies only node_modules + dist onto a small/distroless base, runs as the non-root node user, uses exec-form CMD ["node","dist/main.js"], and handles signals (--init/tini). Set NODE_ENV=production and a V8 heap limit matching the container.

CI runs typecheck + lint + tests on every push (the loop); use npm ci with a committed lockfile; design atomic, zero-downtime deploys and let the orchestrator restart — don't bundle a process manager inside k8s.

How to say it “Multi-stage image, non-root, proper signal handling, NODE_ENV=production, npm ci with a lockfile, and CI gating on typecheck/lint/test.”

34 · Graceful shutdown & health checks

Core: call app.enableShutdownHooks(), then on SIGTERM: fail readiness first (stop new traffic), stop accepting connections, finish in-flight requests, close DB/Redis/brokers, flush logs, exit. Nest awaits promises from the shutdown hooks.

Probes (@nestjs/terminus): liveness = process alive (cheap, no DB) → restart on failure; readiness = can serve now (checks real deps) → removed from the load balancer on failure; startup guards slow boots. In k8s, add a small preStop delay since endpoint removal and SIGTERM race.

How to say it “On SIGTERM I fail readiness, drain in-flight work, then close resources — and I keep liveness cheap so a slow DB doesn't trigger a restart loop.”

35 · Versioning, compression & file handling

Core: API versioning via app.enableVersioning({ type }) — URI (/v1, default), header, media-type, or custom — with @Version('1') per controller/route and VERSION_NEUTRAL for shared routes. Compression (Express compression / @fastify/compress) is best offloaded to a reverse proxy at scale.

File upload: Multer via FileInterceptor + ParseFilePipe (size + magic-number type validation); Multer is incompatible with Fastify (use @fastify/multipart). Streaming files: return a StreamableFile so post-controller interceptors still run.

How to say it “URI versioning, ParseFilePipe to validate uploads by size and magic number, StreamableFile for downloads, and compression at the proxy.”

36 · Soft skills & system-design interviews

What they want: communication, structured thinking, and trade-off awareness. In a design round, clarify requirements (functional + non-functional, scale, read/write ratio) before drawing — most candidates skip this — then sketch a high-level design, deep-dive one component, discuss trade-offs out loud (“I'm trading freshness for speed”), and summarize risks.

For behavioral questions, use STAR (Situation, Task, Action, Result) with a concrete metric. Explain trade-offs to non-technical stakeholders in business terms, and disagree-and-commit in reviews.

How to say it “I clarify before I design, name the trade-off for every decision, and tie behavioral stories to a measurable result.”