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.
Handler factories and closures
Build a function named like the route that closes over your dependencies and returns the inner handler Stario registers:
from stario import Context, Writer, responses 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": # Setup: open connections, … return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: object | None, ) -> None: # Teardown: close connections, … 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. Database is a synchronous context manager (__enter__ / __exit__); HttpClient is asynchronous (__aenter__ / __aexit__), like many HTTP clients that need async setup.
from collections.abc import AsyncIterator from stario import App, Span async def bootstrap(app: App, span: Span) -> AsyncIterator[None]: span.attr("app.name", "users-api") with Database() as db: async with HttpClient() as http: app.get("/users", list_users(db, http)) app.get("/users/{id}", 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.
Bootstrap shapes: setup-only vs teardown
You do not have to use an async generator with a single yield. A common shape is async def bootstrap(app: App, span: Span) -> None: that registers routes and returns—dependencies built in the function body (or module scope) stay alive for the process; there is no framework-driven teardown phase after bootstrap returns until shutdown. The chat-room example under projects/stario/examples/chat-room in the Stario source tree uses that pattern.
Use an async generator (async def bootstrap(...) -> AsyncIterator[None] with one yield) or an async context manager when you need cleanup after the server stops: code after yield or after the async with body runs on shutdown (see Application lifecycle). Pick yield when you want Database.__exit__ / HttpClient.__aexit__ to run in a defined order when the process winds down.
Class-based handlers and routers
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 / handle.
You can also give that class a method that builds a Router (with shared middleware or a prefix) and mount it on the app. The patterns compose; this page only sketches a few so you can choose what fits.
The Database and HttpClient stubs are the same types as in the closure example above.
from collections.abc import AsyncIterator from stario import App, Context, Router, Span, Writer, responses 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) def routes(self) -> Router: r = Router() r.get("/users", self.list_users) r.get("/users/{id}", self.get_user) return r async def bootstrap(app: App, span: Span) -> AsyncIterator[None]: with Database() as db: async with HttpClient() as http: users = UserHandlers(db, http) # Option A — bound methods on the main app app.get("/users", users.list_users) app.get("/users/{id}", users.get_user) # Option B — same instance, routes under a prefix (pick A or B, not both) # app.mount("/api", users.routes()) yield # Shutdown — HttpClient.__aexit__, then Database.__exit__.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 from stario import Context, Writer, responses @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 collections.abc import AsyncIterator from stario import App, Span async def bootstrap(app: App, span: Span) -> AsyncIterator[None]: with Database() as db: async with HttpClient() as http: svc = Services(db=db, http=http) app.get("/", dashboard(svc)) yield # Shutdown — HttpClient.__aexit__, then Database.__exit__.Dependencies 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).
You push each resource onto the stack as you open it; when the stack exits (after yield in your bootstrap generator, or when leaving an async with block), it unwinds everything in reverse order, like nested context managers. That keeps teardown correct and the startup block short. It is only a convenience—explicit nested with / async with is equally valid.
from collections.abc import AsyncIteratorfrom contextlib import AsyncExitStack from stario import App, Span async def bootstrap(app: App, span: Span) -> AsyncIterator[None]: 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("/users/{id}", get_user(db, http)) yield # Shutdown — stack unwinds: HttpClient.__aexit__ first, then Database.__exit__ (reverse of enter order).On AsyncExitStack, synchronous managers use stack.enter_context(...); asynchronous ones use await stack.enter_async_context(...). One stack can mix both so bootstrap stays flat instead of nesting async with and with.
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. Putting a pool on c.state every time would be unusual; putting the current user or session claims there after middleware runs is the usual split.
The pool (or engine) should stay per app; sessions or connections checked out for work should be opened only where they are needed—usually inside the handler (or a helper it calls)—not acquired in middleware and held for the entire request. Typical pattern: acquire just before the queries you need, then release before unrelated work—holding a session across unrelated work ties it up longer than necessary (your ORM may use a different unit-of-work style; adapt to your stack).
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 —
bootstrapshapes, shutdown,TestClientAuthentication with cookie sessions — session on
c.state,AuthSessioninbootstrapStructuring larger applications — file layout and feature routers
Telemetry — spans around queries and outbound calls
Testing —
TestClientagainst the samebootstrap