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.”