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 InvalidTokenError(
    HTTPError,
    type="invalid-token",
    title="Invalid token",
    status=401,
): ...


class WidgetNotFoundError(
    HTTPError,
    type="widget-not-found",
    title="Widget not found",
    status=404,
): ...


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 InvalidTokenError()
        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 WidgetNotFoundError() 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 WidgetNotFoundError()
        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()
        widget_service = await self._factory.create_widget_service()
        self._include_resource(WidgetResource(widget_service), 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 real modules — config, models, auth, services/, operations/, factory, app — see Project structure: a walkthrough of the demo_app/ package, the widget app fleshed out with background analytics, reverse-routed links, health checks, and streaming, and the app the test suite runs against.