Serving an OpenAPI contract¶
httk.serve.http.openapi turns a caller-owned OpenAPI 3.1 document into a
running HTTP application. You own the contract — the OpenAPI document and its
JSON Schemas — and supply one function per operation; the adapter derives the
routes, methods, status codes, media types, and request/response validation
from the contract. Whatever cannot be derived is checked when the application is
constructed, not when a request arrives, so a handler that does not match
its contract fails at startup rather than in production.
It implements a deliberately small, offline subset of OpenAPI — GET/POST,
JSON request/response bodies, and string path/query/header parameters. It is not
a general OpenAPI implementation; the exact boundaries are in
OpenAPI adapter details.
From schema to server¶
Four steps take an OpenAPI contract to a live application:
Ship the contract — the OpenAPI document and its
*.jsonschemas as package data (conventionallyschemas/openapi.yamlalongsideschemas/).Load it into an
OpenAPIContract.Write handlers — one callable per
operationId, returning plain JSON values. Parameters are filled by name from the contract.Build the app with
create_openapi_app, then mount or run it.
from httk.serve.http.openapi import OpenAPIContract, create_openapi_app, operation
# 1-2. Load the packaged OpenAPI document + JSON Schemas (parsed and validated
# once, then cached).
CONTRACT = OpenAPIContract.from_package("prototype_protocol")
# 3. One handler per operationId. Each parameter is filled by name from a
# declared path/query/header parameter or the request body; the return value
# is validated against the operation's declared response before it is sent.
async def create_thing(body):
return await service.create(body) # -> the declared 2xx body
async def get_thing(thing_id):
return await service.get(thing_id)
# 4. Build the application. It returns a ServeApp (a mountable ASGI
# application) — no Starlette import required.
app = create_openapi_app(
CONTRACT,
{
"create_thing": create_thing,
# `operation()` adjusts how a handler binds to its contract, e.g. when a
# wire parameter name should map to a different Python parameter.
"get_thing": operation(get_thing, aliases={"id": "thing_id"}),
},
request_error_handler=prototype_request_error,
)
Serve app with any ASGI server:
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8080)
The only required argument beyond the contract and operations is
request_error_handler, which turns a malformed request into your protocol’s
own error response. Everything else — a per-request scope, deliberate protocol
exception handlers, route converters — is optional and covered in the details.
A worked, real-world example¶
httk.serve.dsp is a complete implementation of this pattern: it ships a DSP
OpenAPI contract as package data, binds each operation to a provider method, and
builds the app with create_openapi_app (see httk/serve/dsp/api.py). Because
the contract accessors let an implementation be tested against its own document,
httk.serve.dsp checks that the errors and media types written in Python cannot
drift from the contract (tests/test_dsp_contract_agreement.py).
Details¶
The details page covers packaged contracts and the plain-mapping alternative, the response contracts derived from each operation, how handler parameters are bound by name, the per-request scope (and post-response callbacks), and error handling.