Problem or use case
Consumers building real APIs on OxyRoute (e.g. QueryaHub/MarketApi) need a usable interactive API explorer out of the box — the same DX FastAPI provides with /docs + /redoc.
Today OxyRoute only exposes a minimal GET /openapi.json (see docs/openapi.md). That document is enough for discovery, but not enough for Scalar / Swagger UI without app-side glue:
| Gap |
Impact |
No built-in docs UI (/docs, /api/docs, Scalar, Swagger UI) |
Every app reimplements HTML + CDN + CSP |
Paths use matchit :param instead of OpenAPI {param} |
Scalar/Swagger do not bind path params correctly |
No parameters entries for path params |
Try-it-out is incomplete |
No components.securitySchemes / per-op security from require_jwt |
Authorize / Bearer JWT missing in UI |
No tags / servers / richer info hooks |
Flat, hard-to-navigate spec |
Docs show async def __rsgi_init__, but Granian RSGI calls sync __rsgi_init__(loop) with a non-running loop |
Lifespan silently skipped (coroutine was never awaited); pools never open |
MarketApi currently works around this with an enriched /openapi.json, Scalar at /api/docs, Swagger at /api/swagger, and a sync Granian-compatible lifespan — that glue belongs in the framework.
Proposed solution
1. Docs UI (P0)
Ship an official optional UI (prefer Scalar, also fine to offer Swagger UI):
App(..., include_openapi=True, docs_ui="scalar" | "swagger" | None)
- or helpers:
app.mount_docs("/docs", ui="scalar") / include_docs_ui=True
- Default route:
GET /docs (and optionally /redoc / /swagger)
- Spec URL: existing
/openapi.json
- Document CSP notes when loading UI from CDN, or vendor a small static bundle via
StaticFiles
2. OpenAPI enrichment (P0)
When building / serving the document:
- Convert path templates
:name → {name} and emit OpenAPI parameters (in: path, required: true).
- If a route was registered with
require_jwt=True, attach:
components.securitySchemes.bearerAuth (http + bearer + JWT)
security: [{ bearerAuth: [] }] on that operation (public routes keep security: [] or omit global security).
- Support tags from
include_router(..., tags=[...]) or an explicit openapi_tags= / route tags= kwarg.
- Allow setting
servers, info.description, info.contact from Python (set_openapi_info(...) / constructor kwargs).
Keep body_schema / body_model as today; document them as the path to rich request bodies in the UI.
3. Lifespan contract (P0 — docs + helper)
Align docs and base App with the Granian RSGI spec:
def __rsgi_init__(self, loop):
loop.run_until_complete(self._async_startup())
- Update docs/rsgi.md and examples (
examples/rsgi_lifespan_app.py) — async override alone is wrong under Granian.
- Optionally provide a base helper that apps can override as
async def on_startup(self) / on_shutdown, while the framework’s sync __rsgi_init__ / __rsgi_del__ call loop.run_until_complete.
- Keep TestClient behavior: no-arg call may still return/await a coroutine.
4. Nice-to-haves (P1)
- Response content schemas / status codes beyond bare
200
- Multipart upload documented for
read_form_body routes
- Export enriched spec via
openapi_json() (same document the UI uses)
- CHANGELOG entry for the OpenAPI/docs surface in the next minor
Acceptance criteria
Context
Priority
P0 for DX — without this, every OxyRoute API reimplements FastAPI-like docs and hits the Granian lifespan footgun.
Problem or use case
Consumers building real APIs on OxyRoute (e.g. QueryaHub/MarketApi) need a usable interactive API explorer out of the box — the same DX FastAPI provides with
/docs+/redoc.Today OxyRoute only exposes a minimal
GET /openapi.json(see docs/openapi.md). That document is enough for discovery, but not enough for Scalar / Swagger UI without app-side glue:/docs,/api/docs, Scalar, Swagger UI):paraminstead of OpenAPI{param}parametersentries for path paramscomponents.securitySchemes/ per-opsecurityfromrequire_jwtinfohooksasync def __rsgi_init__, but Granian RSGI calls sync__rsgi_init__(loop)with a non-running loopcoroutine was never awaited); pools never openMarketApi currently works around this with an enriched
/openapi.json, Scalar at/api/docs, Swagger at/api/swagger, and a sync Granian-compatible lifespan — that glue belongs in the framework.Proposed solution
1. Docs UI (P0)
Ship an official optional UI (prefer Scalar, also fine to offer Swagger UI):
App(..., include_openapi=True, docs_ui="scalar" | "swagger" | None)app.mount_docs("/docs", ui="scalar")/include_docs_ui=TrueGET /docs(and optionally/redoc//swagger)/openapi.jsonStaticFiles2. OpenAPI enrichment (P0)
When building / serving the document:
:name→{name}and emit OpenAPIparameters(in: path,required: true).require_jwt=True, attach:components.securitySchemes.bearerAuth(http+bearer+JWT)security: [{ bearerAuth: [] }]on that operation (public routes keepsecurity: []or omit global security).include_router(..., tags=[...])or an explicitopenapi_tags=/ routetags=kwarg.servers,info.description,info.contactfrom Python (set_openapi_info(...)/ constructor kwargs).Keep
body_schema/body_modelas today; document them as the path to rich request bodies in the UI.3. Lifespan contract (P0 — docs + helper)
Align docs and base
Appwith the Granian RSGI spec:examples/rsgi_lifespan_app.py) — async override alone is wrong under Granian.async def on_startup(self)/on_shutdown, while the framework’s sync__rsgi_init__/__rsgi_del__callloop.run_until_complete.4. Nice-to-haves (P1)
200read_form_bodyroutesopenapi_json()(same document the UI uses)Acceptance criteria
docs_ui(or equivalent) enabled, opening/docsin a browser shows Scalar or Swagger against/openapi.jsonwithout app-specific HTML./openapi.jsonuses{param}paths and lists pathparameters.granian --interface rsgi(DB/redis init actually runs; no “never awaited” warning).200+text/html.Context
app/docs/openapi_enrich.py+app/routers/docs/(Scalar + Swagger) + sync__rsgi_init__(loop)Priority
P0 for DX — without this, every OxyRoute API reimplements FastAPI-like docs and hits the Granian lifespan footgun.