Skip to content

Getting started

jero targets Python 3.13+ and runs under any ASGI server.

Install

uv add jero

You'll also want an ASGI server to run it. granian is a good default, and the granian extra installs both in one step:

uv add "jero[granian]"

Python version

jero requires Python 3.13 or newer. Its generics use type-parameter defaults (PEP 696, e.g. JSONResponse[T: Struct, H: Struct | None = None]), which shipped in 3.13. If you need an earlier version, get in touch on GitHub Discussions.

Your first app

A jero app is a BaseApp subclass that wires up resources (REST collections) and endpoints (single routes). Handler inputs and outputs are msgspec Structs — the types are the request/response contract.

jero has no route decorators. Instead of writing @app.get(...), you define a class, declare its path on the class, and let method names carry the HTTP semantics.

from msgspec import Struct

from jero import BaseApp, Resource


class WidgetPath(Struct):
    widget_id: str


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


class WidgetResource(Resource, path="/widgets"):
    # GET /widgets/{widget_id}
    async def read_one(self, path: WidgetPath) -> Widget:
        return Widget(id=path.widget_id, name="widget-name")


class App(BaseApp):
    async def wire(self) -> None:
        self._include_resource(WidgetResource())


app = App()

Run it:

granian --interface asgi myapp:app
curl localhost:8000/widgets/abc # -> { "id": "abc", "name": "widget-name" }

That's the whole loop: a Struct for the URL slots (path), a Struct for the response, and a method name (read_one) that maps to GET.

Everything that crosses the wire is a Struct — bodies, params, path slots, headers, forms, auth users. The Struct is what gives jero validation, fast serialization, startup checks, and the OpenAPI spec; a raw dict return is a startup error. Philosophy has the full reasoning.

The mental model

  • A Resource is a class with any of the CRUD methods create / read_one / read_many / update_full / update_partial / delete, mapped to POST / GET (item) / GET (collection) / PUT / PATCH / DELETE. See Resources & Endpoints.
  • An Endpoint is a class with bare verb methods (get/post/…) for non-resource routes — health checks, webhooks, actions.
  • Handler arguments bind by name, each a Struct: json, params, path, headers, form, user, plus raw content: bytes / raw_headers. See Request binding.
  • Returns are typed: a Struct, list[Struct], bytes, or a response wrapper (JSONResponse[T], BytesResponse, a streaming response) when you need to control headers or status. See Responses & headers.
  • Dependencies are hand-wired in wire — no DI container. The framework adds the one thing plain Python doesn't: resource lifecycle. See Wiring & lifecycle.
  • For a complete application shape, see the complete example.

Test it without a server

jero ships a synchronous, in-process TestClient — no socket, no running server:

from jero.testing import TestClient


def test_read_one():
    with TestClient(App()) as client:
        resp = client.get("/widgets/abc")
        assert resp.status_code == 200
        assert resp.json() == {"id": "abc", "name": "widget-name"}

Where next

Everything else — streaming, forms, auth, errors, deployment — is in the Guide.