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:

python
import stario.responses as responses
from 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 handler

bootstrap opens dependencies, registers routes, yields while the server runs, then runs cleanup:

python
from stario import App, Span
from 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.

python
import stario.responses as responses
from stario import App, Context, Span, Writer
from 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)
            yield

For 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:

python
from dataclasses import dataclass
 
import stario.responses as responses
from 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 handler
python
from stario import App, Span
from 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))
            yield

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).

python
from contextlib import AsyncExitStack
 
from stario import App, Span
from 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.