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:

  1. Ship the contract — the OpenAPI document and its *.json schemas as package data (conventionally schemas/openapi.yaml alongside schemas/).

  2. Load it into an OpenAPIContract.

  3. Write handlers — one callable per operationId, returning plain JSON values. Parameters are filled by name from the contract.

  4. 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.