Lifecycle
Hook into the moments an app comes alive and winds down — #[hooks] tags async methods for the init and shutdown phases the framework drains in order.
A binary has a beginning and an end. Between the container being built and the first request served, providers may need to warm a cache, open a long-lived connection, or check a migration ran. When the process receives a signal, those same providers want a chance to flush, drain, or report before the runtime tears down. The lifecycle hooks are where that work hangs.
A provider opts in by tagging methods on an impl block with
#[hooks]. Each tagged method names exactly one phase. The framework
collects every hook at link time and drains them phase by phase — init
phases after wiring and before serving, shutdown phases after the
transports stop.
use nest_rs::core::{hooks, injectable};
#[injectable]#[derive(Default)]pub struct BlogService;
#[hooks]impl BlogService { #[on_application_bootstrap] async fn warm(&self) -> anyhow::Result<()> { tracing::info!(target: "features::blog", entries = 0, "cache warmed"); Ok(()) }
#[on_application_shutdown] async fn flush(&self) { tracing::info!(target: "features::blog", pending = 0, "buffers flushed"); }}The #[hooks] block is separate from the one #[injectable] decorates
— a provider keeps its primary impl for methods, and adds a second,
hook-only impl the macro scans. The phase attributes
(#[on_application_bootstrap], …) need no import; #[hooks] consumes
them.
The host must be a singleton provider — the phases resolve it
with Container::get::<Host>(), outside any request, which answers for
exactly one registration shape. Three hosts can never satisfy that, and
all three are compile errors naming the fix rather than a notice at boot:
| Host | Why it cannot |
|---|---|
an edge host (#[controller], #[gateway], #[resolver], #[mcp]) | registers metadata; the instance is built at mount |
#[injectable(scope = request)] | the container holds a factory, not a value |
#[injectable(scope = transient)] | resolution would build a throwaway, and the hook’s effects would be dropped |
Each decorator records which of these it built, as
ProviderResidency::SINGLETON, and #[hooks] reads it. So the refusal
is not a marker you can satisfy by implementing it: contradicting what
your decorator already wrote is a coherence error. A provider you
register by hand with ContainerBuilder::provide is the one case nothing
has spoken for, and it says so itself:
impl ProviderResidency for MyHandWrittenProvider { const SINGLETON: bool = true;}A fourth shape is your composition rather than a contradiction, so it warns
at boot instead. A host the container holds under some other key has
nothing under Foo itself — bound as providers = [Foo as dyn Trait],
imported through a for_root, or registered by hand under a name. Move the methods to a provider listed under its own type.
Listing the same host twice is not the repair it looks like: each
binding runs the constructor, so the hooks fire on one instance while
everything injecting Arc<dyn Trait> holds another.
The five phases
Section titled “The five phases”Two run on the way up, three on the way down. They drain in this fixed order:
| Phase | Attribute | When |
|---|---|---|
| Module init | #[on_module_init] | After the container is built and transports configured, before serving. |
| Application bootstrap | #[on_application_bootstrap] | After every module-init hook has run, still before serving. |
| Module destroy | #[on_module_destroy] | After the transports stop. |
| Before shutdown | #[before_application_shutdown] | After every module-destroy hook has run. |
| Application shutdown | #[on_application_shutdown] | Last — after every before-shutdown hook has run. |
The container is flat, so the init pair isn’t about per-module
resolution order the way a tree-shaped framework would split them —
both run against the assembled container. The two names exist so you
can express “everyone’s on_module_init finishes before anyone’s
on_application_bootstrap starts”: late hooks see the side effects of
early ones. The three shutdown phases give the same staged unwind in
reverse-intent order.
Method shape
Section titled “Method shape”A hook method is async fn and takes &self. Its return is either:
- nothing — the hook is infallible (
async fn flush(&self)), or Result<(), E>whereE: Into<anyhow::Error>— the hook can fail, and the framework folds the error into the phase outcome.
#[on_module_init]async fn check(&self) -> Result<(), MyError> { /* ... */ Ok(()) } // fallible
#[on_module_destroy]async fn drain(&self) { /* ... */ } // infallibleA hook that needs another provider injects it — there is no
attribute to declare “run after OtherService’s hook.” Cross-provider
init dependencies are expressed through the DI graph, not the lifecycle
registry.
Ordering and failure
Section titled “Ordering and failure”Within a phase, hooks run sequentially in (provider, method) name
order — stable across builds, never parallel. The two halves of the
lifecycle treat failure differently, and deliberately:
- Init is strict.
on_module_initthenon_application_bootstraprun one hook at a time; the first error aborts boot with a message naming the provider, method, and phase. Nothing is listening yet, so a failed warm-up never leaves a half-started server accepting traffic. - Shutdown is exhaustive. The three teardown phases run every hook
even if one fails — a failure is logged at
erroron thenest_rs::lifecycletarget and the next hook still runs, so one provider’s botched cleanup can’t skip another’s.
Error: lifecycle hook MigrationGuard::check (OnModuleInit) failed: pending migrationsWhere it sits in the run
Section titled “Where it sits in the run”App::run walks a fixed sequence. The hooks bracket the serving window:
build container→ attach + configure transports→ on_module_init ┐ strict — first error aborts boot→ on_application_bootstrap ┘→ serve (until SIGINT / SIGTERM, or a transport errors)→ on_module_destroy ┐→ before_application_shutdown │ exhaustive — every hook runs, errors logged→ on_application_shutdown ┘Shutdown is triggered by SIGINT / SIGTERM (a shared cancellation
token cancels every transport), or by the first transport that errors —
which cancels the rest, then runs the teardown phases.
Common things a hook does
Section titled “Common things a hook does”- Init — warm a cache, verify migrations applied, register with a service discovery backend, prime a connection pool, assert an external dependency is reachable (fail boot if not).
- Shutdown — flush a buffer, drain in-flight work, deregister from discovery, emit a final metric or report.
The UsersService in the Publish workspace uses
#[on_application_shutdown] to log how many users exist at teardown —
a one-method #[hooks] block beside the service’s main impl.
Going further
Section titled “Going further”- Providers — the things that own hooks;
built once, shared as
Arc<T>. - Modules — the composition tree the hooks drain across.
- Interceptors — per-request wrapping, the other side of “code that runs around your handler” (lifecycle is per-process, interceptors are per-request).
- Testing / e2e — booting the real
AppModule, whereApp::init()drives the init phases.