Skip to content

FAQ

Can handlers be sync?

Yes. Handlers, authenticate, and middleware hooks may all be def or async def — jero detects which at wiring. Use sync for pure CPU-light work; anything that does I/O should be async.

A def handler runs directly on the event loop, not in a thread pool. This is the opposite of FastAPI, which moves every def handler onto a thread pool so that blocking code there is safe. jero doesn't, for performance (see below). While a def handler runs, every other request on that worker waits, so a blocking call inside one (a sync SDK, a time.sleep, a large file read) stalls the whole server. Keep the handler async def and hand the blocking call to a thread yourself, or use an async client instead:

import asyncio
import time

from jero import BaseApp, Endpoint, Struct


class Report(Struct):
    rows: int


def build_report() -> Report:
    time.sleep(0.01)  # stands in for any blocking call
    return Report(rows=1)


class ReportsEndpoint(Endpoint, path="/reports"):
    async def get(self) -> Report:
        return await asyncio.to_thread(build_report)


class App(BaseApp):
    async def wire(self) -> None:
        self._include_endpoint(ReportsEndpoint())


app = App()

There is no implicit thread pool on purpose, and the main reason is performance. Handing every sync handler to a thread would add a thread switch to each request, the CPU-light ones that never needed it included. It would also hide which of your code runs concurrently. asyncio.to_thread puts the hand-off in plain sight, on exactly the calls that need it.

Does jero support WebSockets?

Yes—jero provides typed bidirectional WebSockets and an in-process typed Channel for fan-out. For one-way server push, prefer NDJSON or Server-Sent Events, which cover most live-update cases and give SSE clients automatic reconnection.

Can I serve static files?

Small ones, yes: _include_assets reads a directory once at wiring and serves it from memory, with ETag, gzip, and cache headers all baked at startup. Real static serving (large files, Range requests, catch-all SPA fallbacks) still belongs on your reverse proxy or CDN (see Deployment).

Where is request.state?

There isn't one. Middleware can't pass state to handlers — the idiom is jero's typed sources: bind the header in the handler, or carry the value on the auth user.

Can I return a dict?

No — a JSON body is a Struct (or list[Struct]), and a dict return is a WiringError at startup. The Struct is what buys validation, the OpenAPI schema, and msgspec's encode speed. See Philosophy.

Can I use Pydantic models?

Not on the wire — request and response contracts are msgspec Structs. Off the wire, anything goes: the demo app uses pydantic-settings to parse environment configuration, then maps it into a Struct.

Why does ruff want my Struct imports under TYPE_CHECKING?

jero reads your annotations at startup: a handler's path: WidgetPath or json: WidgetIn, an authenticator's headers: Credentials, a middleware hook's request: Request[H]. They are the binding and the OpenAPI spec. A type used only there looks typing-only to ruff's flake8-type-checking rules (TC001–TC003), which suggest moving its import under if TYPE_CHECKING: (typically once future annotations or a 3.14 target is on). Don't: the name must exist at runtime. If it doesn't, startup fails with a WiringError naming the method and the annotation.

Turn those rules off for your app's modules, as jero's own demo app does:

[tool.ruff.lint.per-file-ignores]
"myapp/**" = ["TC001", "TC002", "TC003"]

ruff's runtime-evaluated-base-classes setting doesn't help here: it exempts class-level field annotations, not method signatures.

Does it run on Python 3.12?

No. jero's generics use PEP 696 type-parameter defaults, which shipped in 3.13. See Getting started.

Why is there no dependency-injection container?

Because constructors already are one: build objects in wire, pass them in. The framework adds only what plain Python lacks — resource lifecycle. See Wiring & lifecycle and Philosophy.

How do I share state between workers?

You don't — each worker process runs wire and holds its own services and background queue. Shared state (a cache, a durable queue) belongs in an external service. See Deployment.

How stable is the API?

jero is in beta, on the 0.x series. The public surface is settled and fully documented, but a breaking change may still land in a minor release until 1.0 — every release's changes are listed in the release notes.

Why is the TestClient sync?

So plain pytest functions work with no async plumbing: it runs the app's full lifespan on a background event loop and gives you a requests-style API. See Testing.