Skip to content

Project structure

The complete example is one file so the whole shape is visible at a glance. A real app splits the same idea into modules — and jero ships one: demo_app/, the project-structured widgets API the test suite runs against, so it is always working and always idiomatic. This page walks its layout.

The layout

demo_app/
├── app.py           # the BaseApp subclass — wire() is the whole wiring story
├── factory.py       # the composition root: builds services on the app's exit stacks
├── config.py        # environment-selected settings
├── models.py        # the Struct contracts: wire models, params, headers, the auth user
├── auth.py          # authenticators (gating + optional, over one shared lookup)
├── errors.py        # error contracts + the custom exception handler
├── services/        # the I/O layer: one class per capability
│   ├── widget_service.py
│   ├── analytics_service.py
│   └── question_service.py
└── operations/      # the routes: Resources & Endpoints grouped by feature
    ├── widget_operations.py
    ├── system_operations.py
    └── streaming_operations.py
Module What lives there Guide
app.py The BaseApp[Factory] subclass. wire builds services from the factory, opens the background queue, and lists every include — the app's whole topology on one screen. Wiring
factory.py The BaseFactory. Each create_* builds one service, entering anything with lifecycle via _aenter. Tests swap the whole thing through the factory= seam. Wiring, Testing
config.py Environment-specific settings. jero ships no settings system; the demo's convention is pydantic-settings for env parsing, mapped into a msgspec Settings Struct the services receive.
models.py Every Struct that crosses the wire, inheriting one Camel base that fixes the wire convention. Binding, OpenAPI
auth.py Two authenticators over one token lookup — one gates, one serves anonymous callers — so a route's policy is visible in what its mount passes. Authentication
errors.py Typed HTTPError contracts and the custom exception handler. Errors
services/ Classes that do I/O. They know nothing about HTTP — they take and return models.
operations/ The Resource / Endpoint classes, one module per feature area. They know nothing about construction — dependencies arrive through __init__. Resources & Endpoints

The dependency direction

Dependencies point one way:

operations  →  services  →  config
     ↑             ↑
   app.py  →  factory.py

operations/ call services/; factory.py constructs services; app.py asks the factory for them and passes them to operations. Nothing imports app.py, and no module reaches sideways for a global — which is why the factory= test seam can replace the entire I/O layer without touching auth, routes, or models.

Adapting it

The names aren't framework contracts — jero only sees what wire includes. What's worth keeping as you rename things:

  • One module owns construction (the factory), so wire stays a short list of includes.
  • Routes take dependencies through __init__, never from module scope.
  • Contracts live together (models.py), inheriting one wire-convention base.
  • Pure, lifecycle-free wiring (like the demo's in-memory token map) can live directly in wire; the factory is for services that open things.