Skip to content

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: BaseModelStruct

# 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: HTTPExceptionHTTPError

# 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 in wire, 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 auth user.
  • Returning dicts. A JSON body is a Struct — a dict return 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

  1. Port models to Structs (mechanical, type-checker-guided).
  2. Group routes into Resource / Endpoint classes; move Depends chains into constructors and wire.
  3. Replace HTTPExceptions with typed errors.
  4. Wire _include_openapi, boot the app, and fix each WiringError it reports — startup validation is the migration checklist running itself.
  5. Point your test suite at jero.testing.TestClient.