How to Implement NestJS Lifecycle Hooks: OnModuleInit and OnModuleDestroy

Implement the OnModuleInit and OnModuleDestroy interfaces in your providers, controllers, or modules to execute custom logic immediately after dependency resolution and immediately before application shutdown.

NestJS provides a built-in lifecycle hook system that lets you run initialization code after all dependencies are resolved and cleanup code when the application begins shutting down. By implementing these interfaces from @nestjs/common, you can manage database connections, seed data, or gracefully close external resources. This guide demonstrates how to implement lifecycle hooks using the actual source code from the nestjs/nest repository.

Understanding the Lifecycle Hook Interfaces

The lifecycle contracts are defined as simple TypeScript interfaces in the common package. Each interface exposes exactly one method that NestJS calls automatically during the application lifecycle.

In packages/common/interfaces/hooks/on-init.interface.ts, the OnModuleInit interface is defined as:

export interface OnModuleInit {
  onModuleInit(): any;
}

Similarly, packages/common/interfaces/hooks/on-destroy.interface.ts defines OnModuleDestroy:

export interface OnModuleDestroy {
  onModuleDestroy(): any;
}

The return type any allows you to return a Promise for asynchronous operations. NestJS detects and awaits promises automatically, ensuring your async initialization completes before the application accepts requests.

How NestJS Executes Lifecycle Hooks

When the application bootstraps, the core module resolver iterates over every provider, controller, and middleware to locate classes implementing these interfaces.

The callModuleInitHook function in packages/core/hooks/on-module-init.hook.ts performs the following steps:

  1. Collects all non-transient and transient instances from the module
  2. Filters instances that implement OnModuleInit
  3. Invokes onModuleInit() on each instance
  4. Finally calls the hook on the module class itself if it implements the interface

The symmetric callModuleDestroyHook in packages/core/hooks/on-module-destroy.hook.ts executes during shutdown, running onModuleDestroy() on every eligible instance before the application process exits.

Both hooks respect static dependency trees (verified via moduleClassHost.isDependencyTreeStatic()) to ensure they execute exactly once per module, avoiding duplicate calls for dynamically scoped providers.

When to Use Each Hook

Hook Execution Timing Common Use Cases
OnModuleInit After all providers, controllers, and middlewares of a module have been instantiated and their dependencies resolved. Initialize database clients, connect to external APIs, seed reference data, warm up caches.
OnModuleDestroy Immediately before app.close() completes, during the application shutdown phase. Close database connections, stop background timers, flush pending writes, unsubscribe from message brokers.

Implementing OnModuleInit in Providers

Providers are the most common place to implement lifecycle hooks. The following example from the NestJS samples shows a Prisma service that establishes its database connection automatically when the module initializes:

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
    console.log('Prisma connected');
  }
}

Source: sample/22-graphql-prisma/src/prisma/prisma.service.ts

By extending PrismaClient and implementing OnModuleInit, the service connects to the database immediately after NestJS instantiates it and resolves any injected dependencies.

Implementing OnModuleDestroy for Cleanup

For resources that require explicit teardown, implement OnModuleDestroy to prevent memory leaks and hanging connections. This Redis client example demonstrates graceful shutdown:

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import * as redis from 'redis';

@Injectable()
export class CacheService implements OnModuleInit, OnModuleDestroy {
  private client: redis.RedisClient;

  onModuleInit() {
    this.client = redis.createClient();
    this.client.on('ready', () => console.log('Redis ready'));
  }

  onModuleDestroy() {
    this.client.quit();
    console.log('Redis connection closed');
  }
}

The onModuleDestroy() method ensures the Redis client closes its connection properly when you call app.close() or the process receives a shutdown signal.

Adding Hooks Directly to Module Classes

You can also implement lifecycle hooks directly in the module class itself. This is useful for module-level initialization logic that coordinates multiple providers:

import { Module, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { CacheService } from './cache.service';

@Module({
  providers: [CacheService],
})
export class AppModule implements OnModuleInit, OnModuleDestroy {
  onModuleInit() {
    console.log('Application initialized');
  }

  onModuleDestroy() {
    console.log('Application shutting down');
  }
}

NestJS detects that AppModule implements these interfaces and invokes the hooks at the appropriate times, just as it does for providers.

Using Hooks in Controllers

While less common, controllers can also implement lifecycle hooks. The following pattern tracks application uptime by recording the initialization timestamp:

import { Controller, Get, OnModuleInit } from '@nestjs/common';

@Controller('status')
export class StatusController implements OnModuleInit {
  private startTime: number;

  onModuleInit() {
    this.startTime = Date.now();
  }

  @Get()
  health() {
    return { uptime: Date.now() - this.startTime };
  }
}

Source: sample/04-grpc/src/hero/hero.controller.ts

Summary

  • Implement OnModuleInit in packages/common/interfaces/hooks/on-init.interface.ts to run code after all module dependencies resolve.
  • Implement OnModuleDestroy in packages/common/interfaces/hooks/on-destroy.interface.ts to clean up resources before the application exits.
  • Execution is automatic via callModuleInitHook and callModuleDestroyHook in the core hooks directory.
  • Return type is any, supporting both synchronous and asynchronous (Promise-based) implementations.
  • Apply hooks to providers, controllers, or modules depending on which scope owns the resource lifecycle.

Frequently Asked Questions

Can lifecycle hooks return Promises for async operations?

Yes. The interfaces define the return type as any, which includes Promises. The core execution logic in on-module-init.hook.ts and on-module-destroy.hook.ts properly awaits any returned promises, ensuring your asynchronous initialization or cleanup completes before the application continues booting or fully terminates.

Does OnModuleDestroy run when the process crashes or only on graceful shutdown?

OnModuleDestroy only executes during graceful shutdown when you explicitly call app.close() or when NestJS catches termination signals (SIGTERM, SIGINT) and triggers its shutdown hooks. It will not run on unhandled exceptions or process crashes that bypass the NestJS shutdown sequence.

Can a single class implement both OnModuleInit and OnModuleDestroy?

Yes. A provider, controller, or module can implement both interfaces simultaneously to handle both initialization and cleanup logic in one location. The NestJS lifecycle system will invoke onModuleInit() during startup and onModuleDestroy() during shutdown for the same instance.

Do lifecycle hooks work with transient-scoped providers?

Yes. The callModuleInitHook and callModuleDestroyHook functions explicitly collect both non-transient and transient instances. However, hooks respect static dependency trees to avoid duplicate execution, ensuring each unique instance receives the hook call exactly once during its lifecycle.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →