Lifecycle hooks
ModuleBase defines ten lifecycle hooks. All are no-ops by default — override the ones you need.
At boot, for each module (in topological order), the framework calls them in this sequence:
register_settings
register_menu_items
register_permissions
register_feature_flags
register_event_handlers
register_health_checks
register_exception_handlers
register_middleware
register_routes
-- on_startup (async, after middleware is installed)On shutdown, on_shutdown runs in reverse dependency order.
register_settings(app)
Called first. Attach per-module state to app.state.<module_lower>. This is where your pydantic-settings object and any runtime-computed services go.
def register_settings(self, app: FastAPI) -> None:
from orders.settings import OrdersEnv
from orders.state import OrdersState
app.state.orders = OrdersState(settings=OrdersEnv())If you override this hook but don't touch app.state.<module_lower>, diagnostic SM012 warns in dev. See Settings & app.state.
register_menu_items(registry)
Add entries to the global MenuRegistry. Items are grouped by MenuSection and role-filtered per request by InertiaLayoutDataMiddleware.
def register_menu_items(self, registry: MenuRegistry) -> None:
registry.add(MenuItem(
section=MenuSection.SIDEBAR,
key="orders",
label_key="orders.menu.orders", # i18n key
href="/orders",
icon="package",
required_permission="orders.view",
order=20,
))required_permission filters the item from users without the permission. order is a stable sort key (lower = earlier).
Sections: SIDEBAR, ADMIN_SIDEBAR, NAVBAR, USER_DROPDOWN.
register_permissions(registry)
Declare permission strings your module enforces. Grouped by a display name for the admin UI.
def register_permissions(self, registry: PermissionRegistry) -> None:
registry.add_group("Orders", [
"orders.view", "orders.create", "orders.edit", "orders.delete",
])Permissions become available in the role admin UI (/settings/permissions). See Permissions.
register_feature_flags(registry)
Declare feature flags with defaults. The admin can toggle them at /settings/feature-flags.
def register_feature_flags(self, registry: FeatureFlagRegistry) -> None:
registry.add("orders.new_checkout", default=False)Query from code:
flags = request.app.state.sm.feature_flags
if flags.is_enabled("orders.new_checkout", user=request.state.principal):
...register_event_handlers(bus)
Subscribe to events on the in-process EventBus. Handlers can be sync or async; the bus awaits async ones.
def register_event_handlers(self, bus: EventBus) -> None:
from orders.contracts.events import OrderPlaced
bus.subscribe(OrderPlaced, self._on_order_placed)
async def _on_order_placed(self, event: OrderPlaced) -> None:
...Dispatch walks the event's MRO, so subscribing to a base class delivers subclass events. See Events.
register_health_checks(registry)
Register named async checks. They're surfaced at /admin/health:
def register_health_checks(self, registry: HealthRegistry) -> None:
registry.add("orders.db", self._check_db)
async def _check_db(self) -> HealthStatus:
...Return HealthStatus.ok() or HealthStatus.failing(detail=...). The health endpoint aggregates all checks and returns 503 if any critical check fails.
register_exception_handlers(app)
Register FastAPI exception handlers scoped to your module's exceptions:
def register_exception_handlers(self, app: FastAPI) -> None:
app.add_exception_handler(OrderNotFound, self._handle_not_found)
async def _handle_not_found(
self, request: Request, exc: OrderNotFound
) -> Response:
return JSONResponse({"detail": str(exc)}, status_code=404)Handlers are registered on the main app, so they apply globally. Keep them tight to types your module owns.
register_middleware(app)
Install ASGI middleware. Starlette's add_middleware is LIFO — the last middleware added runs first. Modules' middleware runs between the framework's built-in middleware and the app itself. See Middleware pipeline for ordering rules.
def register_middleware(self, app: FastAPI) -> None:
app.add_middleware(OrdersRateLimitMiddleware, rate=10)When two modules at the same dependency tier both add middleware, the module that sorts later wraps outermost (executes first).
register_routes(api_router, view_router)
Mount your API and Inertia view routers onto the two framework-provided routers.
def register_routes(
self, api: APIRouter, views: APIRouter
) -> None:
from orders.endpoints.api import router as api_router
from orders.endpoints.views import router as view_router
api.include_router(api_router, prefix="/api/orders")
views.include_router(view_router, prefix="/orders")api_routeris mounted at/apion the main app.view_routeris mounted at/on the main app.
Prefixes in ModuleMeta.route_prefix / view_prefix are documentation; the framework does not auto-apply them. You specify the prefix in include_router.
on_startup() / on_shutdown()
Async lifespan hooks that run after all modules are registered.
async def on_startup(self) -> None:
await self._worker_pool.start()
async def on_shutdown(self) -> None:
await self._worker_pool.stop()on_startupruns in topological order.on_shutdownruns in reverse topological order.
Typical uses:
- Start Celery/RQ consumers (
background_tasks). - Warm caches.
- Register webhooks with external services.
- Probe required dependencies and fail boot on missing ones.
Full example
class OrdersModule(ModuleBase):
meta = ModuleMeta(
name="Orders",
route_prefix="/api/orders",
view_prefix="/orders",
depends_on=["Users", "Products"],
version="1.0.0",
)
def register_settings(self, app): ...
def register_menu_items(self, registry): ...
def register_permissions(self, registry): ...
def register_feature_flags(self, registry): ...
def register_event_handlers(self, bus): ...
def register_health_checks(self, registry): ...
def register_exception_handlers(self, app): ...
def register_middleware(self, app): ...
def register_routes(self, api, views): ...
async def on_startup(self): ...
async def on_shutdown(self): ...If your module overrides zero hooks, diagnostic SM007 (INFO) asks whether the module is still doing anything useful. Empty modules are typically deletable.