Migrating from FastAPI¶
Most FastAPI concepts have a direct jero equivalent — the shape changes, the idea carries over. This page is the map. For why the shapes differ, read Comparison; for a routes-and-dependencies side-by-side, its FastAPI section.
The map¶
| FastAPI | jero |
|---|---|
@app.get("/x") on a function |
An Endpoint class with a get method |
A group of CRUD routes / APIRouter |
A Resource class — method names are the operations |
Pydantic BaseModel |
msgspec Struct (below) |
Field(...) constraints |
Annotated[..., msgspec.Meta(...)] — validated and documented |
Depends(...) |
Constructor arguments, wired in wire |
Query() / Path() / Header() / Cookie() params |
params / path / headers / cookies Structs |
response.set_cookie(...) / response.delete_cookie(...) |
SetCookie(...) / SetCookie.expire(...) on any response |
UploadFile, File(), Form() |
A form Struct with FilePart / FormPart |
HTTPException(404, detail=...) |
A typed HTTPError subclass you raise |
response_model= |
The return annotation — it is the schema |
status_code=201 |
Automatic for create; otherwise a response wrapper |
lifespan / startup events |
wire + _enter / _aenter |
BackgroundTasks per request |
One typed, app-wide BackgroundTasks queue |
| ASGI middleware | The compiled middleware protocol |
CORSMiddleware |
self._include_cors(CORS(...)) |
Auth dependency (Depends(get_user)) |
An authenticator at the mount; handlers declare user |
fastapi.testclient.TestClient |
jero.testing.TestClient — also sync, in-process |
/docs (automatic) |
self._include_openapi(...) — explicit, one line |
Models: BaseModel → Struct¶
# FastAPI / Pydantic
class Widget(BaseModel):
name: str = Field(min_length=1)
price_cents: int
class Config:
alias_generator = to_camel
# jero / msgspec
from typing import Annotated
from msgspec import Meta, Struct
class Widget(Struct, rename="camel"):
name: Annotated[str, Meta(min_length=1)]
price_cents: int
Same contract: validated on decode, camelCase on the wire, constraints in the OpenAPI schema. Give your project one base Struct fixing the wire convention and inherit it everywhere.
Errors: HTTPException → HTTPError¶
# FastAPI
raise HTTPException(status_code=404, detail="widget not found")
# jero
class WidgetNotFoundError(
HTTPError,
type="widget-not-found",
title="Widget not found",
status=404,
): ...
raise WidgetNotFoundError()
Four lines once, instead of a string at every raise site — clients dispatch on the
stable type, and the error documents itself in
the spec. Errors covers parameterized details and custom body
formats.
What doesn't carry over¶
- A DI container. There is no
Depends. Build objects inwire, pass them to constructors — Wiring & lifecycle is the whole story. request.state. Middleware can't pass state to handlers. Bind the header you need in the handler, or carry it on the authuser.- A thread pool for
defhandlers. FastAPI runs adefhandler in a thread pool, so a blocking call there leaves the event loop free. jero runs it on the event loop, so the same blocking call makes every other request wait. Port blocking handlers toasync defandawait asyncio.to_thread(...)the blocking call, or switch to an async client; see Can handlers be sync?. - Returning
dicts. A JSON body is aStruct— adictreturn is a startup error, and that's what buys validation, schema, and speed. - Body-rewriting middleware. Compression, caching, and ETags belong in your server or reverse proxy — see Deployment.
- Static files. Small ones, yes —
_include_assetsserves them from memory; large files and catch-all SPA fallbacks still belong on your proxy or CDN.
Porting order that works¶
- Port models to
Structs (mechanical, type-checker-guided). - Group routes into
Resource/Endpointclasses; moveDependschains into constructors andwire. - Replace
HTTPExceptions with typed errors. - Wire
_include_openapi, boot the app, and fix eachWiringErrorit reports — startup validation is the migration checklist running itself. - Point your test suite at
jero.testing.TestClient.