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)
- Instantiate all providers (DI container)
- Call
on_module_init() on each provider that implements it — per module, in import-graph order (leaves first)
- Call
on_application_bootstrap() on each provider — global, after all modules done
Invocation Order (Shutdown)
- Receive shutdown signal (SIGTERM, SIGINT, or
app.close())
- Call
before_application_shutdown(signal) on each provider
- Call
on_module_destroy() on each provider
- Call
on_application_shutdown(signal) on each provider
- 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
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
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:
DatabaseModulemust establish its connection pool before any service that uses it runsCacheModulemust warm up before request handlers are readyToday 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 whenapp.close()is triggered```python
@Injectable
class DatabaseService(OnModuleInit, OnModuleDestroy):
async def on_module_init(self):
await self.pool.connect()
```
BeforeApplicationShutdown— called beforeOnModuleDestroy, 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)
on_module_init()on each provider that implements it — per module, in import-graph order (leaves first)on_application_bootstrap()on each provider — global, after all modules doneInvocation Order (Shutdown)
app.close())before_application_shutdown(signal)on each provideron_module_destroy()on each provideron_application_shutdown(signal)on each providerapp.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
@Moduledecorated class itself can implementOnModuleInit/OnModuleDestroy. PyNest should support this too — the module class is instantiated and its hooks called alongside provider hooks.Acceptance Criteria
OnModuleInit,OnApplicationBootstrap,OnModuleDestroy,BeforeApplicationShutdown,OnApplicationShutdowninterfaces (Protocol classes) innest/common/interfaces.py@Moduledecorated classes supportedapp.close()method triggers the full shutdown sequenceapp.close()whenapp.enable_shutdown_hooks()is calledImplementation Notes
PyNestFactory.create()after the container is fully built, before returning the appasyncio.gather()for hooks within the same lifecycle phase when order within a phase doesn't matterlifespancontext manager (preferred over deprecatedon_event)contextlib.asynccontextmanagerto wrap the full lifecycle as a FastAPIlifespanRelated