Skip to main content

What are lifecycle hooks in NestJS?

NestJS lifecycle hooks are interfaces that let providers run custom logic at specific points during module initialization and application shutdown.

Theory

TL;DR

  • OnModuleInit fires after a module's providers are ready, before the app starts listening
  • OnApplicationBootstrap fires after ALL modules are initialized, right before app.listen()
  • Shutdown hooks require app.enableShutdownHooks() in main.ts - without it, SIGTERM silently kills the process
  • Execution order: child modules first, then parent, then app-level; shutdown runs in reverse
  • Decision rule: use OnModuleInit for DB connections, OnApplicationBootstrap for tasks that need all modules up

Quick example

typescript
import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; @Injectable() export class DatabaseService implements OnModuleInit { private readonly logger = new Logger(DatabaseService.name); async onModuleInit() { await this.connect(); this.logger.log('DB connected after DI resolved'); // Runs once, after providers are ready, before app.listen() } private async connect() { /* establish connection pool */ } }

NestJS calls onModuleInit once, after the DI container resolves all dependencies for that module. No HTTP request has been accepted at this point.

Module-level vs application-level

There are two categories. Module-level hooks (OnModuleInit, OnModuleDestroy) fire per-module as NestJS bootstraps each one. Application-level hooks (OnApplicationBootstrap, BeforeApplicationShutdown, OnApplicationShutdown) fire once across the whole app, after every module is ready.

This matters when you have cross-module dependencies. If ServiceA needs something from ServiceB defined in a different module, and both implement OnModuleInit, you cannot rely on order inside that hook. Use OnApplicationBootstrap instead. By then, all modules are done.

All five hooks

HookMethodTiming
OnModuleInitonModuleInit()After module's providers are created
OnApplicationBootstraponApplicationBootstrap()After all modules initialized, before listening
OnModuleDestroyonModuleDestroy()After app.close() is called
BeforeApplicationShutdownbeforeApplicationShutdown(signal?)Before connections close, receives OS signal
OnApplicationShutdownonApplicationShutdown(signal?)After all connections are closed

When to use

  • DB or Redis connect: OnModuleInit - providers are ready, no cross-module deps needed
  • Background workers or cron jobs: OnApplicationBootstrap - all modules up, order guaranteed
  • Graceful shutdown (drain pools, flush logs): OnApplicationShutdown + app.enableShutdownHooks()
  • Fast prep before shutdown (stop accepting jobs): BeforeApplicationShutdown, keep it sync or fast async
  • Per-request logic: skip hooks entirely, use guards or interceptors

How it works internally

NestJS scans providers for implemented hook interfaces during module instantiation. It collects them in an ordered list based on scope (module vs app-wide) and calls each via await during NestApplication.init() or close(). For shutdown, process.on('SIGTERM') and process.on('SIGINT') trigger the chain, but only after you call app.enableShutdownHooks(). The DI container tracks which providers implement each interface via Reflect metadata, so the implements keyword is not just a TypeScript formality. Without it on the prototype, NestJS will not call the hook at runtime.

Startup order: child modules' OnModuleInit fires first, then parent modules, then OnApplicationBootstrap across everything. Shutdown: BeforeApplicationShutdown runs first (fast prep), then the server stops accepting connections, then OnModuleDestroy, then OnApplicationShutdown.

Common mistakes

Mistake 1: hooks in request-scoped providers

typescript
// WRONG @Injectable({ scope: Scope.REQUEST }) export class UserService implements OnModuleInit { onModuleInit() { /* connects to DB */ } } // hook fires on every HTTP request - connection leak // RIGHT: move to a singleton-scoped service

Request-scoped providers get created and destroyed per request. The hook fires thousands of times. Move initialization logic to a module-scoped service.

Mistake 2: blocking bootstrap with synchronous heavy work

typescript
// WRONG onModuleInit() { const data = fs.readFileSync('./config.json'); // blocks event loop } // RIGHT async onModuleInit() { const data = await fs.promises.readFile('./config.json'); }

A sync CPU-heavy task in onModuleInit freezes the entire bootstrap. On Heroku or similar platforms, this causes a dyno timeout before the app even starts.

Mistake 3: cross-module deps in OnModuleInit

typescript
// WRONG - OtherModuleService may not be ready yet @Injectable() export class AppService implements OnModuleInit { constructor(@Inject(OtherModuleService) private svc: OtherModuleService) {} onModuleInit() { this.svc.doSetup(); // race condition } } // RIGHT - use OnApplicationBootstrap, all modules guaranteed ready

Mistake 4: missing app.enableShutdownHooks()

typescript
// main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableShutdownHooks(); // without this, shutdown hooks never run await app.listen(3000); }

In every production NestJS codebase I have reviewed, this was the most common omission. The app works fine locally, deploys to Kubernetes, and nobody notices that graceful shutdown never actually runs - until a rolling deploy drops active connections.

Mistake 5: not awaiting async operations in shutdown

typescript
// WRONG - process exits before the promise resolves onApplicationShutdown(signal: string) { closeDB(); // no await } // RIGHT async onApplicationShutdown(signal: string) { if (signal === 'SIGTERM') { await closeDB(); } }

One team lost 10k queued jobs this way before adding async/await to their shutdown hook.

Real-world usage

  • TypeORM: OnModuleInit for dataSource.initialize() - same pattern as the official NestJS docs
  • @nestjs/bull queues: OnApplicationBootstrap starts Redis workers after all modules are up
  • Prisma: BeforeApplicationShutdown for prisma.$disconnect()
  • Winston logger: OnModuleDestroy flushes transports before the process exits
  • Redis cache: OnModuleInit to connect, OnModuleDestroy to drain the pool

Follow-up questions

Q: What is the execution order across multiple modules?


A: Depth-first on startup: child modules' OnModuleInit fires before the parent's. OnApplicationBootstrap runs after all OnModuleInit calls finish. Shutdown reverses the order.

Q: Do lifecycle hooks work in dynamic modules?


A: Yes. Hooks register per instance, so dynamically loaded modules register their hooks when they load. Lazy-loaded modules delay hook registration until LazyModuleLoader.load() is called.

Q: What happens if a hook throws an error?


A: The error bubbles up and aborts the bootstrap. NestJS logs the stack trace. Wrap risky operations in try/catch inside the hook if you want the app to fail gracefully rather than crash.

Q: Why not use process.on('SIGTERM') directly instead of shutdown hooks?


A: You can, but NestJS hooks give you automatic ordering and DI context. Raw process.on listeners run outside the NestJS lifecycle, so you lose the guarantee that all modules finished their work before cleanup runs.

Q: How does NestJS detect which providers implement a hook interface at runtime? (senior-level)


A: It checks prototypes via inspection like 'onModuleInit' in provider combined with Reflect metadata during provider discovery. This means omitting implements OnModuleInit in TypeScript does not stop the hook from firing - if the method exists on the prototype, NestJS calls it.

Examples

Redis with full connect and cleanup

typescript
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { createClient } from 'redis'; @Injectable() export class CacheService implements OnModuleInit, OnModuleDestroy { private client = createClient(); async onModuleInit() { await this.client.connect(); console.log('Redis ready'); } async onModuleDestroy() { await this.client.quit(); // drains in-flight commands before closing socket console.log('Redis pool drained'); } } // On SIGTERM output: Redis pool drained

OnModuleInit and OnModuleDestroy as a pair give you a clean connect/disconnect contract. The quit() call drains in-flight commands before closing the socket, preventing data loss.

Graceful queue shutdown with signal handling

typescript
// main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableShutdownHooks(); // required for SIGTERM/SIGINT await app.listen(3000); } // queue.service.ts import { Injectable, BeforeApplicationShutdown, OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class QueueService implements BeforeApplicationShutdown, OnApplicationShutdown { private accepting = true; beforeApplicationShutdown(signal: string) { console.log(`Signal received: ${signal}`); this.accepting = false; // stop queuing new jobs, fast and sync } async onApplicationShutdown(signal: string) { if (signal === 'SIGTERM') { await this.drainActiveJobs(); // slow async cleanup } console.log('Queue drained, shutting down'); } private drainActiveJobs() { return new Promise<void>(resolve => setTimeout(resolve, 2000)); } }

BeforeApplicationShutdown handles the fast synchronous prep (stopping intake), while OnApplicationShutdown handles the slow async part (draining). Splitting the work this way avoids blocking the shutdown sequence.

Database seeding in development with OnApplicationBootstrap

typescript
import { Injectable, OnApplicationBootstrap } from '@nestjs/common'; @Injectable() export class SeedService implements OnApplicationBootstrap { constructor( private readonly usersService: UsersService, private readonly configService: ConfigService, ) {} async onApplicationBootstrap() { if (this.configService.get('NODE_ENV') !== 'development') return; const count = await this.usersService.count(); if (count === 0) { await this.usersService.create({ name: 'Admin', email: 'admin@app.com' }); console.log('Dev seed complete'); } } }

OnApplicationBootstrap is the right choice here because SeedService depends on UsersService from another module. By the time onApplicationBootstrap fires, the entire DI graph is resolved and every module has finished OnModuleInit.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?