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.
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:
Or with Uvicorn over ASGI:
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.
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.
Read query, header, and cookie parameters¶
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()andCookie()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¶
- Routing - sub-routers, named routes, and catch-all paths.
- Requests & responses - headers, cookies, streaming, files, and redirects.
- Dependencies - advanced dependency patterns.
- Deployment - run your app in production.