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.- 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. jero serves JSON APIs; put assets 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.