Injecting dependencies
The filename says “database,” but the pattern is the same for any long-lived client you open once and reuse (DB pool, HTTP client, cache, …). Databases are the usual motivating example; short-lived work still belongs inside the handler (sessions, cursors), not in globals.
This page shows how handlers get shared objects—here a database and an HTTP client (outbound API, webhook target, …). Stario does not ship a DI framework: you choose a composition root (usually bootstrap), build long-lived clients there, and pass them into handlers with closures, a small bundle type, or a class whose methods are routes. Pick what stays readable in your codebase.
In Stario 4, bootstrap must be an async generator with a single yield—startup before yield, teardown after.
For a full app using this pattern with db + Relay, see Chat room and examples/chat-room (thin app/db.py, feature data.py, register_* in handlers).
Handler factories and closures
Build a function named like the route that closes over your dependencies and returns the inner handler Stario registers:
import stario.responses as responsesfrom stario import Context, Writer class Database: def __enter__(self) -> "Database": # Setup: open pool, configure driver, run migrations, … return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: object | None, ) -> None: # Teardown: close pool, dispose engine, … pass async def list_users(self) -> list[dict[str, object]]: return [] async def fetch_user_by_id(self, user_id: str) -> dict[str, object] | None: return None class HttpClient: """Stand-in for httpx, aiohttp, or a thin wrapper around urllib.""" async def __aenter__(self) -> "HttpClient": return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: object | None, ) -> None: pass def list_users(db: Database, http: HttpClient): async def handler(c: Context, w: Writer) -> None: _ = http # e.g. await http.get("https://example.com/health") rows = await db.list_users() responses.json(w, {"users": rows}) return handler def get_user(db: Database, http: HttpClient): async def handler(c: Context, w: Writer) -> None: user_id = c.route.params["id"] row = await db.fetch_user_by_id(user_id) if row is None: responses.text(w, "Not found", status=404) return responses.json(w, row) return handlerbootstrap opens dependencies, registers routes, yields while the server runs, then runs cleanup:
from stario import App, Spanfrom stario.routing import UrlPath USERS = UrlPath("/users")USER = USERS / "{id}" async def bootstrap(app: App, span: Span): span.attr("app.name", "users-api") with Database() as db: async with HttpClient() as http: app.get(USERS, list_users(db, http)) app.get(USER, get_user(db, http)) yield # Shutdown — HttpClient.__aexit__ first, then Database.__exit__ when leaving the outer with.Each handler shares the same db and http instances—ordinary Python objects, no global registry.
Class-based handlers
Another shape is a class that takes dependencies in __init__ and exposes methods as handlers (same Context, Writer signature). In bootstrap you instantiate once and pass bound methods to app.get.
import stario.responses as responsesfrom stario import App, Context, Span, Writerfrom stario.routing import UrlPath USERS = UrlPath("/users")USER = USERS / "{id}" class UserHandlers: def __init__(self, db: Database, http: HttpClient) -> None: self._db = db self._http = http async def list_users(self, c: Context, w: Writer) -> None: rows = await self._db.list_users() responses.json(w, {"users": rows}) async def get_user(self, c: Context, w: Writer) -> None: user_id = c.route.params["id"] row = await self._db.fetch_user_by_id(user_id) if row is None: responses.text(w, "Not found", status=404) return responses.json(w, row) async def bootstrap(app: App, span: Span): with Database() as db: async with HttpClient() as http: users = UserHandlers(db, http) app.get(USERS, users.list_users) app.get(USER, users.get_user) yieldFor larger apps, expose a register_users(app, db, http) that registers paths from a feature urls.py—see Structuring larger applications.
Several dependencies at once
When you have more than one long-lived thing, group them in a small immutable bundle and pass one object into your factories or class:
from dataclasses import dataclass import stario.responses as responsesfrom stario import Context, Writer @dataclass(frozen=True, slots=True)class Services: db: Database http: HttpClient def dashboard(svc: Services): async def handler(c: Context, w: Writer) -> None: _ = svc.db _ = svc.http responses.text(w, "ok") return handlerfrom stario import App, Spanfrom stario.routing import UrlPath HOME = UrlPath("/") async def bootstrap(app: App, span: Span): with Database() as db: async with HttpClient() as http: svc = Services(db=db, http=http) app.get(HOME, dashboard(svc)) yieldDependencies as context managers
If every long-lived dependency is a context manager—synchronous (__enter__ / __exit__) or asynchronous (__aenter__ / __aexit__)—you can wire bootstrap without a deep chain of with / async with. The stdlib helpers are contextlib.ExitStack (sync only) and contextlib.AsyncExitStack (async, and it can also enter synchronous context managers on the same stack).
from contextlib import AsyncExitStack from stario import App, Spanfrom stario.routing import UrlPath USERS = UrlPath("/users")USER = USERS / "{id}" async def bootstrap(app: App, span: Span): async with AsyncExitStack() as stack: db = stack.enter_context(Database()) http = await stack.enter_async_context(HttpClient()) app.get(USERS, list_users(db, http)) app.get(USER, get_user(db, http)) yield # Shutdown — stack unwinds in reverse enter order.Dependencies vs state
Dependencies are things you typically create once per process (or per app) and reuse on every request: database pools, HTTP clients, configuration objects, signing keys wrapped in a small type. They are not tied to a single HTTP request; handlers reach them through closures, Services, or attributes on a long-lived instance.
State (in Stario, c.state) is per request: a mutable dict middleware and handlers use to pass request-scoped data—who is authenticated, parsed tokens, ids derived from headers or route params, or anything that only makes sense for this request.
Middleware can populate c.state from the request (for example session cookies); shared database access still comes from dependencies you created in bootstrap.
Related
Runtime — Application lifecycle —
bootstrap, shutdown,TestClientStructuring larger applications —
register_*, feature layoutAuthentication with cookie sessions — session on
c.state,AuthSessioninbootstrapTelemetry — spans around queries and outbound calls
Testing —
TestClientagainst the samebootstrap