LearnThatStack Ace your next interview

Nest.js

Assumes NestJS 12. Newer rows name their version Checked Nov 2026 139 code snippets

The model

  • A module groups controllers and providers. Its providers stay private until it exports them and another module imports it.
  • A controller handles requests and a provider holds the logic. Nest's IoC container creates both and injects what each constructor asks for.
  • Injection runs on tokens. A class is its own token. TypeScript erases interfaces, so they need a string or symbol token and @Inject().
  • A request passes through middleware, guards, interceptors and pipes to the handler. The result goes back out through the interceptors.
  • Decorators only add metadata. @Roles() protects nothing until a guard reads it.
Clientsends the requestMiddleware, guardsrun before the restInterceptorswrap what followsPipes, handlercheck, then runINrequestguard returns truenext.handle()OUTresultresponseERRORfalse sends 403throw, then filter
In each box, the first name runs first. Interceptors wrap the pipes and the handler, so they run both ways. A throw ends in an exception filter.

Modules & controllers

the parts of a Nest app and how they connect

Nest or plain Express

Say what Nest addsIt runs on Express, and adds modules, injection, a request pipeline and TypeScript first
Pick oneExpress for a small service, Nest for a larger team

TrapNest doesn't replace Express. It runs on top of Express by default, and can run on Fastify instead.

Decorators

Name the three main kindsClass like @Module(), method like @Get(), parameter like @Body()
Turn them on in tsconfig.jsonexperimentalDecoratorsemitDecoratorMetadata
Write a parameter decoratorcreateParamDecorator()
Attach metadata to a handlerSetMetadata()Reflector.createDecorator()
Combine several into oneapplyDecorators()

Trap@Roles() only stores metadata. It protects nothing until a guard reads it through the Reflector.

Routes & request data

read a request and shape the response

Read the request

Read a path value@Param('id')
Read filters and paging@Query('page')
Read the request body@Body()
Convert a path value to a number@Param('id', ParseIntPipe)

TrapPath and query values always arrive as strings, whatever type the parameter declares.

Set the status code

Change a success code@HttpCode(204)The default is 200, or 201 for POST
Throw to send an error codeNotFoundExceptionHttpException
Pick the code at runtime@Res({ passthrough: true })

Trap@Res() without passthrough turns off Nest's response handling, so interceptors stop working on that route.

Version an API

Turn on versioningapp.enableVersioning()URI is the default type. The others are HEADER, MEDIA_TYPE and CUSTOM
Mark what a route serves@Version('2')
Serve every version alikeVERSION_NEUTRAL

Stream a response

Send a series of messages@Sse('stream')Return an Observable of MessageEvent
Send a large fileStreamableFileKeeps interceptors working, unlike @Res()

TrapA plain @Get() that returns an Observable doesn't stream. Nest sends only the last value.

Upload files

Take one fileFileInterceptor('file')@UploadedFile()
Take up to ten filesFilesInterceptor('files', 10)@UploadedFiles()
Check size and typelimits: { fileSize }ParseFilePipe

TrapBy default, Multer keeps files in memory with no size limit, so always set limits. Use diskStorage() for big files.

Injection & providers

how the container builds and wires your classes

How injection works

Register a classproviders: [UserService]
Inject a non-class value@Inject('CONFIG')

TipTypeScript emits constructor parameter types as metadata. Nest reads it and looks up each token.

Provider types

Swap a token's classuseClass
Bind a ready objectuseValueSuch as constants or a test mock
Build a value with a functionuseFactoryinject: [ConfigService]
Alias another tokenuseExisting
Wait for I/O firstMake useFactory async. Nest delays startup until it resolves

TipA class with async methods is still a synchronous provider, because Nest creates the class at once.

Provider scopes

Share one, the defaultScope.DEFAULT
Make one per requestScope.REQUEST
Make one per consumerScope.TRANSIENT
Set a scope@Injectable({ scope })

TrapRequest scope bubbles up. A controller that injects a request-scoped service is rebuilt on every request too.

Fix "can't resolve" errors and cycles

Fix a dependency the module can't seeAdd it to providers, or import the module that exports it
Fix a provider in importsMove it to providers. Only modules go in imports
Fix a token that's a typeInject a class, or use @Inject() with a custom token
Fix a provider in two modulesRemove the copy and import the feature module
Remove a cycleMove shared logic to a third service, or emit an event
Keep a cycle@Inject(forwardRef(() => PostService))Add it on both sides. Modules wrap the import the same way

TrapWith forwardRef() the creation order is indeterminate, so neither constructor may call the other.

Dynamic modules

Set up for the calling moduleregister()
Set up once and shareforRoot()forRootAsync()
Adjust for one moduleforFeature()

TipA static method builds a DynamicModule from the options the importing module passes in.

The request pipeline

what runs around a route handler, and in what order

Execution order

1 Middleware: app.use(), then module-bound2 Guards: global, controller, route3 Interceptors: global, controller, route4 Pipes: global, controller, route, parameter5 Route handler6 Interceptors: route, controller, global7 Filters, on a throw: route, controller, global

TrapOnly the lowest-level filter that catches an error handles it. The error never reaches another filter.

Middleware or guard

Do work that ignores the routeMiddleware, for logging, request ids or body parsing
Decide if a request goes onA guard. Only it knows the handler, so it can read the roles on it
Write a middleware classNestMiddleware
Apply it in configure()consumer.apply(MyMiddleware).forRoutes('users').exclude()

TipNest middleware works like Express middleware, so most Express packages work too.

TrapIf middleware neither ends the response nor calls next(), the request is left hanging.

Guards

Write a guardcanActivate()Returns a boolean, Promise or Observable
Bind to a controller@UseGuards(AuthGuard)
Bind globallyAPP_GUARDapp.useGlobalGuards() can't inject services

Tipfalse sends 403, through ForbiddenException. To send 401, throw your own exception.

Interceptors and logging

Run the handlernext.handle()It returns an RxJS Observable of the result
Change a result or errormap()catchError()
Answer from a cacheReturn a stored value without calling handle()
Log the route and outcomefinalize()It runs on success and on failure
Log rejected requests tooUse middleware, which runs before guards

TrapA tap() with only a value callback skips requests that throw.

Request timeouts

Limit a handler's timetimeout(3000)The RxJS operator, in an interceptor
Send 408 on a timeoutRequestTimeoutException

TrapA timeout ends the response, not the work. A pending query keeps running unless you cancel it.

ExecutionContext

Get the requestswitchToHttp()switchToWs()switchToRpc()
Get the handler and controllergetHandler()getClass()

TipOne guard can work across HTTP, WebSockets and microservices. getType() says which one is running.

Pipes, validation & errors

stop bad input and control what the client sees

DTOs and ValidationPipe

Add rules to a DTO class@IsEmail()@Min(18)
Run the rulesnew ValidationPipe()A bad body gets 400
Strip undecorated fieldswhitelist: true
Reject them with 400forbidNonWhitelisted: true
Make DTO instancestransform: trueIt also converts path and query strings
Use Zod or similarStandardSchemaValidationPipe

TrapUse a class, not an interface. Interfaces vanish at compile time, and pipes need a real class at runtime.

Exceptions and filters

Add your own error typeextends HttpException
Say what unknown errors send500 with "Internal server error", so internal details don't leak
Shape one error's response@Catch(HttpException)
Catch everything@Catch()
Bind a filter@UseFilters()

Trapapp.useGlobalFilters() runs outside any module, so its filter can't inject anything. Register it as APP_FILTER instead.

Login, roles & security

log in, check roles and limit abuse

Log in with JWT

Sign a token at loginJwtService
Check it with PassportAuthGuard('jwt')validate() fills req.user
Use the Nest team's package@nestjs/authenticationSessions, password hashing, access and refresh tokens

TrapNever hard-code the JWT secret. Load it from configuration.

Check roles with a guard

Store roles on a route@Roles(['admin'])
Read them in the guardgetAllAndOverride()Handler first, then controller
Write a policy class@nestjs/authorization

TipRegister it as APP_GUARD after the authentication guard, so request.user is already set.

TrapA role check can't see the record a request touches. Check "only your own orders" in the service or a policy.

Rate limiting

Set and enforce limitsThrottlerModule.forRoot()ThrottlerGuard
Override, or turn it off@Throttle()@SkipThrottle()

TipWithout the guard, nothing is limited. A client over the limit gets 429.

TrapThe built-in store counts in one process. With several instances, use a shared store such as Redis.

Secure the app

Set security headersapp.use(helmet())
Use built-in ones (12.1+)app.useSecurityHeaders()
Stop CSRF on cookie sessions (12.1+)app.enableCsrfProtection()
Allow trusted originsapp.enableCors()
Stop SQL injectionUse parameterized queries

TipAgainst cross-site scripting, encode output. Filters that strip tags are easy to bypass.

Databases, config & caching

connect to data and keep it fast

Connect a database

Open the connectionTypeOrmModule.forRoot()
Load credentials from configTypeOrmModule.forRootAsync()
Register and inject a repositoryTypeOrmModule.forFeature()@InjectRepository(User)
Use PrismaIt has no Nest package, so expose its client through a provider

TrapTurn synchronize off in production. It changes the schema on every start and can lose data. Use migrations.

Transactions

Commit or roll back automaticallydataSource.transaction()
Take full controlcreateQueryRunner()Release it in finally

TrapAn injected repository runs on another connection, so its writes escape the transaction. Use the transaction's manager.

Configuration

Load .env for all modulesConfigModule.forRoot()isGlobal: true
Read a value, or throwconfig.get('KEY')config.getOrThrow('KEY')
Check every variable at startupvalidationSchema

TrapWhen a key is in the real environment and in .env, the environment value wins.

Caching

Register the cacheCacheModule.register()ttl is in milliseconds
Cache whole responsesCacheInterceptor@CacheKey()@CacheTTL()
Get and set by hand@Inject(CACHE_MANAGER)

TipCacheInterceptor caches only GET routes by default, and routes that inject @Res() can't use it.

TrapThe cache lives in one process by default. With several instances, use Redis through a Keyv adapter.

Unit & end-to-end tests

real injection, fake dependencies

Unit test a service

Build a small moduleTest.createTestingModule().compile()
Mock a dependency{ provide: Dep, useValue }
Mock a repositorygetRepositoryToken(User)
Mock in a new ESM projectvi.fn()Vitest. CommonJS projects get jest.fn()

End-to-end tests and overrides

Start the whole appcreateNestApplication()
Send it requestsrequest(app.getHttpServer())
Swap one imported pieceoverrideProvider(Dep)overrideGuard(AuthGuard)
Mock whatever you didn't registeruseMocker()

TrapUse resolve(), not get(), for request-scoped and transient providers.

Production & performance

run it, document it and keep it fast

Application lifecycle

onModuleInit()dependencies resolvedonApplicationBootstrap()before it listensthe app runsonModuleDestroy()after SIGTERMbeforeApplicationShutdown()onApplicationShutdown()after connections close
Run the last three on a signalapp.enableShutdownHooks()

TrapSignal handling is off by default. Without it, a container that gets SIGTERM stops without closing its connections.

Switch to Fastify

Change the adapternew FastifyAdapter()
Listen in a containerapp.listen(3000, '0.0.0.0')The default is localhost only
Check what breaksExpress-only packages, and raw @Req() or @Res() code

TrapIf most request time goes to the database, a faster HTTP layer changes little. Measure first.

Make it faster

Tune the databaseAdd indexes, avoid N+1 queries, and size the connection pool
Compress responsesapp.use(compression())On busy sites, let Nginx do it
Avoid per-request instancesKeep providers singletons, because request scope bubbles up
Lighten the HTTP layerFastify reaches nearly twice Express's benchmark results

TipMeasure with real traffic before you change anything, so you fix the slowest part first.

API documentation

Serve Swagger UIDocumentBuilderSwaggerModule.setup()
Show a DTO property@ApiProperty()
Skip most @ApiProperty() callsEnable the Swagger CLI plugin in nest-cli.json

TrapTypeScript leaves no metadata for undecorated properties, so a DTO property needs @ApiProperty() to show up.

Health checks

Check a URL and the databaseHealthCheckService.check()HttpHealthIndicatorTypeOrmHealthIndicator
Write a custom checkHealthIndicatorServiceNestJS 12 removed the HealthIndicator base class

TipAll up returns 200. One failure returns 503 and names the failing check.

TrapPoint the liveness probe at a light check, so a slow downstream service doesn't get a healthy instance restarted.

WebSockets & microservices

talk over something other than HTTP requests

WebSocket gateways

Create a gateway@WebSocketGateway()
Handle one event name@SubscribeMessage('msg')
Broadcast to every client@WebSocketServer()this.server.emit()

TipThe value a handler returns goes back to the sender as an acknowledgment.

TrapSeveral Socket.IO instances need the Redis adapter, plus sticky sessions or clients that skip HTTP polling.

Microservices

Start oneNestFactory.createMicroservice()Over TCP, Redis or Kafka. Handlers don't change
Handle a message and reply@MessagePattern()The client calls send()
Handle an event, no reply@EventPattern()The client calls emit(), which publishes at once

Trapsend() returns a cold Observable, so nothing is sent until something subscribes.

Found this useful? Pass it on.
Pro · $10/mo

This sheet stays free. With Pro, you can:

  • Every answer on this sheet. Read the full answer to all 50 questions it links, not just the 6 free ones.
  • My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
  • PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
  • The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.