Skip to content

Feature: Lifecycle Hooks (OnModuleInit, OnApplicationBootstrap, OnModuleDestroy) #113

@ItayTheDar

Description

@ItayTheDar

Overview

PyNest's current lifespan mechanism relies entirely on FastAPI's @http_server.on_event("startup") / on_event("shutdown") — a loose, unstructured coupling between the framework and the ASGI layer. There is no way for individual modules or providers to hook into the application boot sequence in a typed, testable, and ordered manner.

This feature request proposes implementing NestJS-compatible Lifecycle Hooks as interfaces any provider or module class can implement.


Motivation

Real applications need deterministic initialization order:

  • A DatabaseModule must establish its connection pool before any service that uses it runs
  • A CacheModule must warm up before request handlers are ready
  • Background workers must flush their queues before the process exits

Today developers work around this by putting imperative setup inside __init__, which runs during DI container construction — not after the full module graph is resolved. This leads to race conditions and ordering bugs.


Proposed Lifecycle Interfaces

```python
from nest.common.interfaces import (
OnModuleInit,
OnApplicationBootstrap,
OnModuleDestroy,
BeforeApplicationShutdown,
OnApplicationShutdown,
)
```

OnModuleInit — called after module's providers are instantiated

```python
from nest.common.interfaces import OnModuleInit

@Injectable
class DatabaseService(OnModuleInit):
async def on_module_init(self):
await self.pool.connect()
print("DB pool ready")
```

OnApplicationBootstrap — called after ALL modules are initialized

```python
@Injectable
class CacheWarmupService(OnApplicationBootstrap):
async def on_application_bootstrap(self):
await self.cache.warm_up()
```

OnModuleDestroy — called when app.close() is triggered

```python
@Injectable
class DatabaseService(OnModuleInit, OnModuleDestroy):
async def on_module_init(self):
await self.pool.connect()

async def on_module_destroy(self):
    await self.pool.disconnect()

```

BeforeApplicationShutdown — called before OnModuleDestroy, receives the signal

```python
@Injectable
class WorkerService(BeforeApplicationShutdown):
async def before_application_shutdown(self, signal: str):
print(f"Received {signal}, draining queue...")
await self.queue.drain()
```

OnApplicationShutdown — called after all destroys complete

```python
@Injectable
class TelemetryService(OnApplicationShutdown):
async def on_application_shutdown(self, signal: str):
await self.flush_spans()
```


Invocation Order (Boot)

  1. Instantiate all providers (DI container)
  2. Call on_module_init() on each provider that implements it — per module, in import-graph order (leaves first)
  3. Call on_application_bootstrap() on each provider — global, after all modules done

Invocation Order (Shutdown)

  1. Receive shutdown signal (SIGTERM, SIGINT, or app.close())
  2. Call before_application_shutdown(signal) on each provider
  3. Call on_module_destroy() on each provider
  4. Call on_application_shutdown(signal) on each provider
  5. Close the ASGI server

app.close() API

```python
app = PyNestFactory.create(AppModule)
http_server = app.get_http_server()
uvicorn.run(http_server)

Graceful shutdown:

await app.close()
```


Module-level hooks (optional, lower priority)

In NestJS, the @Module decorated class itself can implement OnModuleInit / OnModuleDestroy. PyNest should support this too — the module class is instantiated and its hooks called alongside provider hooks.


Acceptance Criteria

  • OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown interfaces (Protocol classes) in nest/common/interfaces.py
  • Container detects and calls hooks in the correct order during boot
  • Container detects and calls hooks in the correct order during shutdown
  • Both sync and async hook methods supported
  • Hooks on @Module decorated classes supported
  • app.close() method triggers the full shutdown sequence
  • Signal handlers (SIGTERM, SIGINT) wired to app.close() when app.enable_shutdown_hooks() is called
  • Unit tests covering boot/shutdown order with multi-module graphs
  • Integration test verifying DB-style connect/disconnect lifecycle

Implementation Notes

  • The boot hooks should be called from PyNestFactory.create() after the container is fully built, before returning the app
  • Use asyncio.gather() for hooks within the same lifecycle phase when order within a phase doesn't matter
  • The shutdown sequence should integrate with FastAPI's lifespan context manager (preferred over deprecated on_event)
  • Consider using Python's contextlib.asynccontextmanager to wrap the full lifecycle as a FastAPI lifespan

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions