Skip to content

Getting started

This tutorial walks you from a fresh install to a small but complete VelociPy app. Along the way you'll see how the framework reads the terrain - validation, routing, dependencies, testing, and configuration - and how to pick the right claws for the job.

Installation

The core framework has one runtime dependency: httpx2. Everything else is optional.

pip install velocipy

Optional power-ups

Install the extras that match your stack:

# Fast JSON + model validation
pip install "velocipy[msgspec]"

# Pydantic models
pip install "velocipy[pydantic]"

# Fast JSON encoder
pip install "velocipy[orjson]"

# Multipart uploads
pip install "velocipy[multipart]"

# Redis-backed rate limiting
pip install "velocipy[redis]"

# Granian server with RSGI/ASGI and reload support
pip install "velocipy[granian]"

# Uvicorn server via ASGI
pip install "velocipy[uvicorn]"

Start with msgspec

We recommend pip install "velocipy[msgspec]" for local development. It gives you the fastest JSON encoder and a lightweight struct-based model backend without pulling in a large dependency tree.

Your first app

Create main.py. The smallest hunt is just an app and a route:

from velocipy import VelociPy

app = VelociPy()

@app.get("/")
async def root():
    return {"hello": "world"}

Run it:

granian main:app --interface rsgi --reload

Or with Uvicorn over ASGI:

uvicorn main:app --reload

Open http://localhost:8000/ and http://localhost:8000/docs.

Add typed path parameters

VelociPy uses the path template to infer types. A parameter like {item_id} is matched as a string, {item_id:int} as an integer, and {price:float} as a float.

from velocipy import VelociPy

app = VelociPy()

@app.get("/items/{item_id:int}")
async def read_item(item_id: int):
    return {"item_id": item_id}

@app.get("/prices/{price:float}")
async def read_price(price: float):
    return {"price": price}

See examples/routing.py for named routes, sub-routers, and catch-all patterns.

Validate request bodies

Declare a request body with a type-hinted parameter. VelociPy auto-detects msgspec.Struct and pydantic.BaseModel, or you can register a custom adapter.

from msgspec import Struct
from velocipy import VelociPy

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

app = VelociPy()

@app.post("/items")
async def create_item(item: Item):
    return {"created": item}
from pydantic import BaseModel
from velocipy import VelociPy

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

app = VelociPy()

@app.post("/items")
async def create_item(item: Item):
    return {"created": item}

Body models are detected automatically

You don't need a special Body(...) marker for JSON bodies. VelociPy sees the model type and reads it from the request body.

See examples/model_backends.py for custom adapters and mixed backends.

Use Query, Header, and Cookie to pull values out of the request without touching the raw Request object.

from typing import Annotated
from velocipy import Cookie, Header, Query, VelociPy

app = VelociPy()

@app.get("/items")
async def list_items(
    q: Annotated[str, Query(...)],
    limit: Annotated[int, Query(10)] = 20,
    x_request_id: Annotated[str | None, Header()] = None,
    theme: Annotated[str | None, Cookie()] = None,
):
    return {"q": q, "limit": limit, "x_request_id": x_request_id, "theme": theme}
  • Query(...) makes the parameter required.
  • Query(10) supplies a default and validates the type.
  • Header() and Cookie() read from the corresponding request sources.

See examples/query_params.py and examples/headers_cookies.py for aliases, lists, and optional values.

Dependencies: share the hunt

Use Depends to reuse logic across endpoints. Dependencies can themselves depend on other dependencies, and generator dependencies can clean up after the response.

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Annotated

from velocipy import Depends, Request, VelociPy

app = VelociPy()

async def current_user(request: Request) -> str:
    token = request.header("authorization") or ""
    if not token.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Not authenticated")
    return token.split(" ", 1)[1]

async def page_params(
    page: Annotated[int, Query(ge=1)] = 1,
    size: Annotated[int, Query(le=100)] = 20,
):
    return {"page": page, "size": size}

@app.get("/items")
async def list_items(
    user: Annotated[str, Depends(current_user)],
    params: Annotated[dict, Depends(page_params)],
):
    return {"user": user, "params": params}

See examples/dependencies.py for sub-dependencies, generator teardown, and dependency overrides.

Test in-process

VelociPy ships with TestClient and AsyncTestClient. They work against both ASGI and RSGI, so you can verify your hunt without starting a server.

from velocipy import VelociPy
from velocipy.testing import TestClient

app = VelociPy()

@app.get("/")
def index():
    return {"hello": "world"}

def test_index():
    with TestClient(app) as client:
        response = client.get("/")
        assert response.status_code == 200
        assert response.json() == {"hello": "world"}

See examples/testing.py for async tests and RSGI-interface tests.

Configure the app

VelociPy apps accept a small set of constructor options:

from velocipy import VelociPy
from velocipy.adapters.json.msgspec import MsgspecJSONAdapter

app = VelociPy(
    title="My API",
    version="1.0.0",
    docs_url="/docs",
    openapi_url="/openapi.json",
    max_content_length=10_485_760,  # 10 MB, automatic 413 beyond this
    json_encoder=MsgspecJSONAdapter(),
)
Option What it does
title API title used in OpenAPI docs.
version API version used in OpenAPI docs.
debug In debug mode, unhandled exceptions are re-raised instead of returning 500.
lifespan An async context manager that runs startup/shutdown code.
docs_url Path for Swagger UI; set to None to disable.
openapi_url Path for the OpenAPI JSON endpoint; set to None to disable.
max_content_length Global body-size limit; exceeding it returns 413 Payload Too Large.
multipart_max_memory_size In-memory threshold for multipart file data.
json_encoder Explicit JSON adapter, e.g. MsgspecJSONAdapter, ORJSONAdapter, StdlibJSONAdapter.

See examples/json_encoder.py for encoder selection and examples/basic.py for lifespan usage.

Summary

At a glance, here's what you learned:

Step What to remember
Install pip install velocipy plus the extras you need.
Create an app app = VelociPy().
Add routes @app.get("/items/{item_id:int}").
Validate bodies Type-hint a msgspec.Struct or pydantic.BaseModel.
Read parameters Use Query, Header, and Cookie with Annotated.
Share logic Use Depends for reusable, testable dependencies.
Test Use TestClient(app) with no server required.
Configure Pass title, json_encoder, max_content_length, etc.

Next steps