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]"
For production on Linux, Deployment recommends peregrine, the server the benchmarks run.
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.
Python 3.13, 3.14, and 3.15 are tested in CI against the full test suite and the project's type checks.
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, etc. 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
Resourceis a class with any of the CRUD methodscreate/read_one/read_many/update_full/update_partial/delete, mapped to POST / GET (item) / GET (collection) / PUT / PATCH / DELETE. See Resources & Endpoints. - An
Endpointis 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,cookies,form,user, plus rawcontent: bytes/content_stream/raw_headers. See Request binding. - Returns are typed: a
Struct,list[Struct],str,bytes, or a response wrapper (JSONResponse[T],BytesResponse, a streaming response, etc.) 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:
# doc-example: fragment (illustrative snippet, not a runnable app)
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¶
- Resources & Endpoints — the routing model and path templates.
- Request binding — every way to get data into a handler.
- Complete example — factory, auth, lifecycle, and typed responses together.
Everything else — streaming, forms, auth, errors, deployment, etc. — is in the Guide.