Injecting dependencies
Open long-lived clients once (DB pool, HTTP client, cache). Pass them into handlers. Short-lived work (sessions, cursors) stays inside the handler.
Stario does not ship a DI framework. bootstrap is the composition root: startup before yield, teardown after. Chat room uses this pattern with db and Relay. See Chat room.
Handler factories
A function named like the route closes over dependencies and returns the inner handler:
import stario.responses as responsesfrom stario import App, Context, Route, Span, UrlPath, Writer USERS_PATH = UrlPath("/users")USERS = Route.get(USERS_PATH)USER = Route.get(USERS_PATH / "{id}") def list_users(db): async def handler(c: Context, w: Writer) -> None: rows = await db.list_users() responses.json(w, {"users": rows}) return handler def get_user(db): async def handler(c: Context, w: Writer) -> None: row = await db.fetch_user_by_id(c.route.params["id"]) if row is None: responses.text(w, "Not found", status=404) return responses.json(w, row) return handler async def bootstrap(app: App, span: Span): # Database is your pool or engine type. with Database() as db: app.add(USERS, list_users(db)) app.add(USER, get_user(db)) yieldEach handler shares the same db instance. No global registry.
Other shapes
A class that takes dependencies in __init__ and exposes methods as handlers works the same way. In bootstrap, instantiate once and pass bound methods to app.add.
When you have more than one long-lived client, group them in a small frozen dataclass (Services) and pass one object into the factories.
If every client is a context manager, contextlib.AsyncExitStack avoids a deep with chain. Enter sync clients with stack.enter_context, async clients with await stack.enter_async_context, then yield. The stack unwinds in reverse enter order.
For larger apps, expose register_users(app, db) that registers paths from a feature urls.py. See Structuring apps.
Dependencies vs state
Dependencies live for the process. Handlers reach them through closures, Services, or attributes on a long-lived instance.
c.state is per request. Middleware puts request-scoped data there (who is authenticated, parsed tokens). Shared database access still comes from objects you created in bootstrap.