{"id":50936742,"url":"https://github.com/rogerthomas/jero","last_synced_at":"2026-06-22T15:00:59.002Z","repository":{"id":365090055,"uuid":"1270517660","full_name":"RogerThomas/jero","owner":"RogerThomas","description":"An opinionated, msgspec-first ASGI micro-framework","archived":false,"fork":false,"pushed_at":"2026-06-20T13:17:28.000Z","size":941,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-20T13:28:06.930Z","etag":null,"topics":["api","asgi","asgi-framework","async","frameworks","granian","json","msgspec","python","python3","rest"],"latest_commit_sha":null,"homepage":"https://rogerthomas.github.io/jero/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RogerThomas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-15T19:43:02.000Z","updated_at":"2026-06-20T13:17:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/RogerThomas/jero","commit_stats":null,"previous_names":["rogerthomas/jero"],"tags_count":31,"template":false,"template_full_name":null,"purl":"pkg:github/RogerThomas/jero","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RogerThomas%2Fjero","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RogerThomas%2Fjero/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RogerThomas%2Fjero/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RogerThomas%2Fjero/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RogerThomas","download_url":"https://codeload.github.com/RogerThomas/jero/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RogerThomas%2Fjero/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34610832,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","asgi","asgi-framework","async","frameworks","granian","json","msgspec","python","python3","rest"],"created_at":"2026-06-17T10:00:19.202Z","updated_at":"2026-06-21T14:00:54.413Z","avatar_url":"https://github.com/RogerThomas.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"docs/assets/jero-logo.png\" alt=\"jero\" width=\"440\"\u003e\n\n\u003cp\u003e\n  \u003ca href=\"https://pypi.org/project/jero/\"\u003e\u003cimg src=\"https://img.shields.io/pypi/v/jero\" alt=\"PyPI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/RogerThomas/jero/actions/workflows/main.yml?query=branch%3Amain\"\u003e\u003cimg src=\"https://github.com/RogerThomas/jero/actions/workflows/main.yml/badge.svg?branch=main\" alt=\"Build status\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://codecov.io/gh/RogerThomas/jero\"\u003e\u003cimg src=\"https://img.shields.io/codecov/c/github/RogerThomas/jero/main\" alt=\"codecov\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://pypi.org/project/jero/\"\u003e\u003cimg src=\"https://img.shields.io/pypi/pyversions/jero\" alt=\"Python versions\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/RogerThomas/jero/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/pypi/l/jero\" alt=\"License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n**An opinionated, msgspec-first ASGI micro-framework for Python 3.14.**\n\n\u003cem\u003eProbably about as fast as idiomatic Python gets. Typed end to end. A joy to build on.\u003c/em\u003e\n\n\u003ca href=\"https://github.com/RogerThomas/jero/\"\u003eGitHub\u003c/a\u003e · \u003ca href=\"https://RogerThomas.github.io/jero/\"\u003eDocumentation\u003c/a\u003e\n\n\u003c/div\u003e\n\n**jero** builds typed JSON/REST APIs from plain classes. Annotate your handlers with\n[msgspec](https://jcristharif.com/msgspec/) Structs — jero does the rest: routing,\nvalidation, serialization, auth, streaming, and resource lifecycle.\n\n```python\nfrom msgspec import Struct\n\nfrom jero import BaseApp, Resource\n\n\nclass Widget(Struct):\n    id: str\n    name: str\n\n\nclass WidgetPath(Struct):\n    widget_id: str\n\n\nclass Widgets(Resource):\n    async def read_one(self, path: WidgetPath) -\u003e Widget:  # GET /widgets/{widget_id}\n        return Widget(id=path.widget_id, name=\"gizmo\")\n\n\nclass App(BaseApp):\n    async def _wire(self) -\u003e None:\n        self._include_resource(Widgets(), path=\"/widgets\")\n\n\napp = App()\n```\n\n```bash\ngranian --interface asgi myapp:app    # or uvicorn, or any ASGI server\n```\n\nNo decorators, no `dict` returns, no runtime surprises — the `Struct` types *are* the\nrequest/response contract, and they're verified at startup.\n\n## Why jero?\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd\u003e⚡\u0026nbsp;\u003cstrong\u003eFast\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eCo-leads the fastest Python ASGI frameworks — within a few percent of Go on the hot path (see \u003ca href=\"#performance\"\u003ePerformance\u003c/a\u003e). All introspection happens once, at startup; the request path is just dict lookup → decode → call → encode.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e🎯\u0026nbsp;\u003cstrong\u003eOpinionated\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eOne blessed way to do each thing, so you can't get it wrong. Contracts fail loud at startup with a precise \u003ccode\u003eWiringError\u003c/code\u003e, never quietly at runtime.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e🔒\u0026nbsp;\u003cstrong\u003eTyped\u003c/strong\u003e\u003c/td\u003e\n\u003ctd\u003eFully static under pyright-strict, leaning hard into modern Python typing — PEP 695 generics (\u003ccode\u003eJSONResponse[Body, Headers]\u003c/code\u003e, \u003ccode\u003eBaseApp[Factory]\u003c/code\u003e), bounded type-params, generic inheritance, \u003ccode\u003eProtocol\u003c/code\u003es. A handler's signature \u003cem\u003eis\u003c/em\u003e its schema, and the source of the coming OpenAPI spec.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\nNo DI container, either: dependencies are hand-wired in `_wire`; the framework adds\nonly lifecycle — the one thing plain Python doesn't give you.\n\n## What you get\n\n- **Resources \u0026 Endpoints** — REST CRUD by method name, or bare verbs for one-off routes.\n- **Bind by name, validated by msgspec** — `json`, `params`, `path`, `headers`, `form`,\n  `user`; malformed → 400, schema-invalid → 422, all resolved once at startup.\n- **Typed responses *and* typed headers** — `JSONResponse[Body, Headers]` keeps both\n  schemas (no erasure), `status_code` overrides the status, and `raw_headers` is the\n  escape hatch for cookies and the exotic tail.\n- **Streaming, typed end to end** — NDJSON, Server-Sent Events, and raw byte streams,\n  with lifecycle teardown and client-disconnect handling done for you.\n- **Multipart forms \u0026 uploads** — typed parts, file uploads, per-part headers.\n- **Auth checked at startup** — the `user` type is verified against your authenticator\n  before a single request is served, not at runtime.\n- **Lifecycle without a DI container** — hand-wire in `_wire`, open resources on exit\n  stacks, group construction in a `BaseFactory`.\n- **REST semantics for free** — 404/400/422/401/405, auto `HEAD` + `OPTIONS`, camelCase\n  on the wire.\n- **A real test story** — a sync, in-process `TestClient` (no socket), streaming support,\n  and a `factory=` seam for mocking.\n\nStart with **[Getting Started](https://RogerThomas.github.io/jero/getting-started/)**, or\nbrowse the full [Guide](https://RogerThomas.github.io/jero/).\n\n## A real app\n\nFor anything real, a resource delegates to a service, and a `Factory` builds that\nservice — opening any resources it needs (HTTP clients, DB pools, …) on the app's\nexit stacks, which jero closes in reverse at shutdown. The app is parameterised with\nthe factory type (`BaseApp[Factory]`), exposing it as `self._factory` in `_wire`.\n\n```python\nfrom dataclasses import dataclass\n\nimport httpx\nfrom msgspec import Struct\nfrom msgspec.json import decode as json_decode\nfrom msgspec.json import encode as json_encode\n\nfrom jero import BaseApp, BaseFactory, HTTPError, Resource\n\n\nclass WidgetPath(Struct):\n    widget_id: str\n\n\nclass WidgetIn(Struct):\n    name: str\n\n\nclass Widget(WidgetIn):\n    id: str\n\n\n@dataclass\nclass WidgetService:\n    \"\"\"Owns the upstream HTTP client; built once by the factory.\"\"\"\n\n    _client: httpx.AsyncClient\n\n    async def fetch(self, widget_id: str) -\u003e Widget:\n        resp = await self._client.get(f\"/widgets/{widget_id}\")\n        if resp.status_code == 404:\n            raise HTTPError(404, \"widget not found\")\n        return json_decode(resp.content, type=Widget)\n\n    async def create(self, data: WidgetIn) -\u003e Widget:\n        resp = await self._client.post(\"/widgets\", content=json_encode(data))\n        return json_decode(resp.content, type=Widget)\n\n\n@dataclass\nclass WidgetResource(Resource):\n    _service: WidgetService\n\n    # called as: POST /widgets\n    async def create(self, json: WidgetIn) -\u003e Widget:\n        return await self._service.create(json)\n\n    # called as: GET /widgets/{widget_id}\n    async def read_one(self, path: WidgetPath) -\u003e Widget:\n        return await self._service.fetch(path.widget_id)\n\n\nclass Factory(BaseFactory):\n    async def create_widget_service(self) -\u003e WidgetService:\n        client = await self._aenter(httpx.AsyncClient(base_url=\"https://api.example.com\"))\n        return WidgetService(client)\n\n\nclass App(BaseApp[Factory]):\n    async def _wire(self) -\u003e None:\n        widget_service = await self._factory.create_widget_service()\n        self._include_resource(WidgetResource(widget_service), path=\"/widgets\")\n\n\napp = App()\n```\n\n## Performance\n\njero is fast — very fast. It co-leads the quickest Python ASGI frameworks, and on a\nnarrow, favorable benchmark lands within a few percent of a hand-written Go (Gin)\nservice. That near-Go figure is a best case under specific conditions — **not** a\nclaim that jero is as fast as Go in general. It isn't, and we're not saying it is.\n\nThe numbers below are from the authed write path — `POST /movies` (bearer auth →\nmsgspec decode → handler → encode → `201`) — run natively under granian with a single\nworker (Go pinned to `GOMAXPROCS=1`), driven by [oha](https://github.com/hatoo/oha)\nat concurrency 200:\n\n| Framework | Requests/sec | Relative to jero |\n| --- | --: | --: |\n| Go / Gin *(reference)* | ≈ 45,200 | 1.03× |\n| **jero** | **≈ 44,000** | **1.00×** |\n| Blacksheep | ≈ 43,000 | 0.98× |\n| Litestar | ≈ 22,000 | 0.50× |\n| Robyn | ≈ 15,000 | 0.34× |\n| FastAPI | ≈ 7,300 | 0.17× |\n\nA statistical tie with Blacksheep, ~2× Litestar, ~3× Robyn, and ~6× idiomatic\nFastAPI — at ~97% of raw Go on the same machine (and ~91% on a plain `GET`). Those\nnear-Go ratios hold **only** under these ideal, constrained conditions — single\nworker, Go pinned to one core, localhost, this one hot path, partly client-bound.\nTreat them as indicative, not a general \"as fast as Go\" claim; the benchmark harness\nlives in a separate repo.\n\n## Development\n\n```bash\ntask install   # create the venv and install pre-commit hooks\ntask check     # lock check + ruff, pyright, deptry, pylint (via prek)\ntask test      # run the test suite with coverage\n```\n\nSee [`AGENTS.md`](AGENTS.md) for the design philosophy and the contract, and\n[`style-guide.md`](style-guide.md) for project conventions.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogerthomas%2Fjero","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frogerthomas%2Fjero","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogerthomas%2Fjero/lists"}