Lifecycle EventsFundamentals · NestJS

Open a connection in onModuleInit and seed data in onApplicationBootstrap, and see from the recorded order why the two hooks exist.

What you will learn

Read the theory for Lifecycle Events

All Fundamentals lessons

All NestJS courses

loading types…

What you'll learn

  • Implement OnModuleInit and OnApplicationBootstrap and place each hook in the boot sequence
  • Explain why hooks of one module run together, and why work that needs another provider ready belongs in onApplicationBootstrap
  • Describe the shutdown hooks, what enables them, and why a browser run never reaches them

Lifecycle Events

An application is not ready the moment its classes exist. A connection must be opened, a cache warmed, seed data written, and at the other end a connection should be closed before the process goes away. Lesson 3 did the first kind of work in an async provider. Lifecycle hooks do it inside the classes that own the resource: a method with a known name that Nest calls at a known point in the application's life, and waits for.

The sequence

An application passes through three phases, initialising, running, terminating, and Nest calls one hook at each step:

HookWhen
onModuleInit()this module's dependencies are resolved and its providers exist
onApplicationBootstrap()every module has initialised, just before the application starts listening
onModuleDestroy()a termination signal arrived, or app.close() was called
beforeApplicationShutdown()every onModuleDestroy() has completed; connections are still open
onApplicationShutdown()connections are closed; the last thing that runs

A class opts in by implementing the interface of the same name from @nestjs/common and writing the method:

@Injectable()
export class SearchIndex implements OnModuleInit {
  constructor(private readonly client: SearchClient) {}

  async onModuleInit(): Promise<void> {
    await this.client.connect();
  }
}

Hooks may be async, and Nest awaits them: a Promise returned from onModuleInit() delays everything after it. The interface is optional at runtime, since Nest looks for the method by name, but it is what makes a typo a compile error rather than a hook that silently never runs.

Who runs first

Two rules decide the order, and both matter for the exercise.

Across modules, hooks run in dependency order: a module's onModuleInit hooks run after those of every module it imports. CatsModule initialises before the AppModule that imports it, so by the time the root module's providers wake up, the feature modules are ready.

Within a module, Nest 12 groups a module's providers by their hierarchy level, their depth in the module's dependency tree, and runs the hooks one level at a time, awaiting each level before the next. A provider that depends on nothing is level 0 and goes first; a provider that injects it comes after, and its onModuleInit can rely on the dependency's hook having finished, even an async one. Providers on the same level run together, in parallel. Earlier Nest versions ran a whole module's hooks in parallel, which is why older advice says never to rely on order within a module; in Nest 12 the order follows the dependencies, and only providers that do not depend on each other are unordered.

onApplicationBootstrap then runs, module by module in the same order, and only after every onModuleInit in the application has completed. That is the difference between the two hooks and the reason both exist: onModuleInit is for getting this class ready, and it may rely on what this class depends on; onApplicationBootstrap is for work that needs the whole application ready, whatever module it lives in.

Shutting down

The three shutdown hooks run only when the application is asked to stop: app.close() in code, or a signal such as SIGTERM from the platform, which Nest listens for only after app.enableShutdownHooks() in main.ts, because listening costs a little memory. The signal is passed to the hooks, so a class can tell a deploy from a crash:

@Injectable()
export class Queue implements OnApplicationShutdown {
  async onApplicationShutdown(signal?: string): Promise<void> {
    await this.drain();
    console.log(`queue drained on ${signal}`);
  }
}

In this browser runtime a run ends by discarding the worker, and nothing is ever closed, so the shutdown hooks never fire here. In a container that receives SIGTERM before being killed, they are how in-flight work finishes.

Your task

The shelter's database must be opened before anything is stored, and the shelter starts with two cats.

  1. Open the database in the hook that runs once CatsModule's dependencies are resolved. Record database:opening before and database:open after.
  2. In CatsService, record cats:init in the same hook, and seed Tom (3) and Luna (5) in the hook that runs once every module has initialised, recording cats:seeded after.

Announcer in the root module records app:init and app:bootstrap, and GET /cats/lifecycle returns everything in the order it happened. Read the order before you decide where the seed goes: putting it in onModuleInit would work, since the database's hook has finished by then, but it would run before the root module has initialised, and the order shows it.

When it fails

  • the database is not open, thrown at boot: the seed ran before the database's hook completed. Either the seed is in a hook that runs too early, or onModuleInit in Database does not await connect().
  • cats:seeded comes before app:init: the seed is in onModuleInit. It works, and it is the wrong hook: it did not wait for the rest of the application.
  • A hook never runs: the method's name is misspelt. Implement the interface and the compiler will tell you.
  • database:open comes after cats:init: onModuleInit in Database returns before the connection is open, because connect() was called without await.

Remember

  • onModuleInit when this module's dependencies are ready; onApplicationBootstrap when every module is; async hooks are awaited.
  • Across modules, imports first. Within a module, dependencies first, level by level; unrelated providers in parallel.
  • Shutdown hooks need app.close() or a signal plus enableShutdownHooks(), and receive the signal.
  • Implement the interface so a misspelt hook is a compile error.
Stuck? Show a hint

Database implements OnModuleInit: an async onModuleInit() that records 'database:opening', awaits connect(), records 'database:open'. CatsService implements OnModuleInit and OnApplicationBootstrap: onModuleInit() records 'cats:init'; onApplicationBootstrap() creates Tom and Luna, then records 'cats:seeded'. The seed has to be in the bootstrap hook: the recorded order proves which hook it ran in.