Skip to content

OpenAPI

A documented API is a trail other hunters can follow. VelociPy auto-generates OpenAPI 3.1 specs from your routes, parameters, and models, then serves a Swagger UI page at /docs and the raw schema at /openapi.json. You can also layer explicit documentation on top when you want finer control over descriptions, examples, response shapes, and tags.

See examples/basic.py and examples/route_extras.py for runnable demos.

The default map

By default, VelociPy scans every route and builds an OpenAPI document from:

  • route paths and HTTP methods
  • path, query, header, and cookie parameters
  • request and response models (msgspec.Struct, pydantic.BaseModel, or custom adapters)
  • return annotations and response_model
from msgspec import Struct
from velocipy import VelociPy

app = VelociPy(title="Hunt API", version="1.0.0")

class Item(Struct):
    name: str
    price: float

@app.get("/items")
async def list_items() -> list[Item]:
    return [Item(name="spear", price=12.5)]

Start the app and visit:

  • http://localhost:8000/docs - Swagger UI
  • http://localhost:8000/openapi.json - raw OpenAPI 3.1 schema

The default endpoints are configured with docs_url and openapi_url:

app = VelociPy(
    title="Hunt API",
    version="1.0.0",
    docs_url="/docs",          # set to None to disable Swagger UI
    openapi_url="/openapi.json",  # set to None to disable the schema endpoint
)

Tip

Setting both URLs to None removes the built-in documentation endpoints entirely. Useful for private microservices that should not expose their schema.

Tags, summaries, and deprecation

Route decorators accept OpenAPI-flavored metadata directly. VelociPy uses the first line of the docstring as the summary when you do not provide one.

from velocipy.status import HTTP_201_CREATED

@app.post(
    "/items",
    tags=["items"],
    summary="Create an item",
    description="Adds a new item to the hunting pack.",
    operation_id="createItem",
    status_code=HTTP_201_CREATED,
)
async def create_item(item: Item) -> dict[str, Item]:
    return {"created": item}

@app.get("/legacy", deprecated=True)
async def legacy() -> dict[str, str]:
    return {"note": "this trail is fading"}

Note

operation_id becomes the stable identifier that code generators use for method names. Keep it unique and deterministic.

Explicit OpenAPI objects

When a docstring and decorator fields are not enough, attach an OpenAPI object to the route. This lets you declare multiple response statuses, named examples, request examples, and override the request body model independently of the handler signature.

from velocipy import OpenAPI, VelociPy
from velocipy.status import HTTP_200_OK, HTTP_404_NOT_FOUND

app = VelociPy()

class User(Struct):
    id: int
    name: str

get_user_doc = OpenAPI(
    summary="Get a user",
    description="Returns a single user by ID.",
    tags=["users"],
    responses={
        HTTP_200_OK: User,
        HTTP_404_NOT_FOUND: dict,
    },
    examples={
        HTTP_200_OK: {"id": 1, "name": "Akela"},
        HTTP_404_NOT_FOUND: {
            "missing": {"detail": "User not found"},
        },
    },
)

@app.get("/users/{user_id}", openapi=get_user_doc)
async def get_user(user_id: int) -> User:
    return User(id=user_id, name="Akela")

Overriding the request body

Sometimes the request body you want documented differs from the model used in the handler. Use request_model and request_example:

create_item_doc = OpenAPI(
    summary="Create an item",
    tags=["items"],
    request_model=Item,
    request_example={"name": "cloak", "price": 29.99},
    responses={HTTP_201_CREATED: dict},
)

@app.post("/items", openapi=create_item_doc)
async def create_item(payload: dict) -> dict[str, str]:
    return {"status": "created"}

Response models

There are two ways to tell OpenAPI what a successful response looks like:

  1. Return annotation - the simplest path.
  2. response_model= argument - useful when the handler returns a plain dict but you want a validated/documented schema.
@app.get("/items/{item_id}", response_model=Item)
async def get_item(item_id: int) -> dict[str, object]:
    return {"id": item_id, "name": "fang", "price": 4.0}

VelociPy also emits a reusable HTTPValidationError component under #/components/schemas/HTTPValidationError whenever a route has path parameters or a request body, so clients know what a 422 looks like.

Security schemes

Security helpers (APIKeyHeader, APIKeyQuery, APIKeyCookie, OAuth2PasswordBearer) integrate with Depends and are reflected automatically in the OpenAPI document under components/securitySchemes. See examples/security.py and the Security page for the full hunt.

from typing import Annotated
from velocipy import Depends, VelociPy
from velocipy.security import APIKeyHeader

api_key = APIKeyHeader(name="X-API-Key")

@app.get("/protected")
async def protected(
    key: Annotated[str, Depends(api_key)],
) -> dict[str, str]:
    return {"status": "authorized", "key": key}

Reusable schemas

Model classes are registered once under #/components/schemas and referenced with $ref. If two models share a class name, VelociPy appends a numeric suffix (User_2) to keep the spec valid.

At a glance

Lever What it does
docs_url / openapi_url Configure or disable built-in docs endpoints
tags, summary, description Route decorator metadata
operation_id Stable operation identifier
deprecated=True Mark a route as deprecated
response_model=... Document and validate the response shape
OpenAPI(...) Explicit responses, examples, and request overrides
responses={status: Model} Declare status-code response models
examples={status: ...} Attach single or named examples
request_model / request_example Override request body documentation

VelociPy writes the map while you run, but it never forces you onto a single trail. Use annotations for speed, route metadata for clarity, and OpenAPI objects when you need to draw the territory exactly as clients should see it.