Skip to content

Complete example

This is a small but complete jero app shape: factory, lifecycle-managed service, authentication, path binding, JSON binding, typed response headers, a resource, and _wire.

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass

from msgspec import Struct

from jero import BaseApp, BaseFactory, HTTPError, JSONResponse, Resource


class Credentials(Struct):
    authorization: str


class User(Struct):
    id: str
    name: str


class WidgetPath(Struct):
    widget_id: str


class WidgetIn(Struct):
    name: str


class Widget(WidgetIn):
    id: str
    owner_id: str


class WidgetHeaders(Struct, omit_defaults=True):
    x_trace_id: str | None = None


class TokenAuth:
    def authenticate(self, headers: Credentials) -> User:
        token = headers.authorization.removeprefix("Bearer ").strip()
        if token != "token":
            raise HTTPError(401, "invalid token")
        return User(id="user-id", name="user-name")


@dataclass
class WidgetStore:
    _widgets: dict[str, Widget]

    async def get(self, widget_id: str) -> Widget:
        try:
            return self._widgets[widget_id]
        except KeyError:
            raise HTTPError(404, "widget not found") from None

    async def list_for_user(self, user: User) -> list[Widget]:
        return [widget for widget in self._widgets.values() if widget.owner_id == user.id]

    async def create(self, user: User, widget: WidgetIn) -> Widget:
        widget_id = f"widget-{len(self._widgets) + 1}"
        created = Widget(id=widget_id, owner_id=user.id, name=widget.name)
        self._widgets[created.id] = created
        return created


@dataclass
class WidgetService:
    _store: WidgetStore

    async def get_widget(self, user: User, widget_id: str) -> Widget:
        widget = await self._store.get(widget_id)
        if widget.owner_id != user.id:
            raise HTTPError(404, "widget not found")
        return widget

    async def list_widgets(self, user: User) -> list[Widget]:
        return await self._store.list_for_user(user)

    async def create_widget(self, user: User, widget: WidgetIn) -> Widget:
        return await self._store.create(user, widget)


@asynccontextmanager
async def open_widget_store() -> AsyncIterator[WidgetStore]:
    widgets = {
        "widget-1": Widget(id="widget-1", owner_id="user-id", name="first-widget"),
    }
    yield WidgetStore(widgets)


class Factory(BaseFactory):
    async def create_widget_service(self) -> WidgetService:
        store = await self._aenter(open_widget_store())
        return WidgetService(store)


@dataclass
class WidgetResource(Resource, path="/widgets"):
    _service: WidgetService

    async def create(
        self,
        json: WidgetIn,
        user: User,
    ) -> JSONResponse[Widget, WidgetHeaders]:
        widget = await self._service.create_widget(user, json)
        return JSONResponse(json=widget, headers=WidgetHeaders(x_trace_id="trace-id"))

    async def read_many(self, user: User) -> list[Widget]:
        return await self._service.list_widgets(user)

    async def read_one(self, path: WidgetPath, user: User) -> Widget:
        return await self._service.get_widget(user, path.widget_id)


class App(BaseApp[Factory]):
    async def _wire(self) -> None:
        auth = TokenAuth()
        widgets = await self._factory.create_widget_service()
        self._include_resource(WidgetResource(widgets), auth=auth)


app = App()

The important part is where the framework boundary sits. The app constructs normal Python objects in _wire, enters anything with lifecycle through the factory, and then includes route classes. Handlers only declare typed inputs by name: json for the request body, path for URL slots, and user for the authentication result.

Run it with an ASGI server:

granian --interface asgi myapp:app

Then call it with a bearer token:

curl -H "Authorization: Bearer token" localhost:8000/widgets/widget-1
# {"name":"first-widget","id":"widget-1","ownerId":"user-id"}

A project-structured version

The example above is a single file so the whole shape is visible at a glance. For the same idea split into the layout a real app would use (config, models, auth, services/, operations/, factory, and app modules), see the demo_app/ package in the repository. It's the widget app fleshed out with authentication, background analytics, reverse-routed links, health checks, and streaming (an OpenAI-backed NDJSON endpoint and a Server-Sent Events feed).

demo_app is also the app the test suite runs against (see the testing approach), so it is always kept working, and as a typed consumer of the public API it is type-checked by every major type checker in CI.