cheat sheet
litestar
Build fast, type-safe HTTP APIs and web apps with Litestar. Covers route handlers, path/query/body params, DTOs, dependency injection, middleware, WebSockets, and OpenAPI.
litestar — High-Performance ASGI Framework
What it is
Litestar (formerly Starlette-Lite / Starlite) is a fully-featured ASGI web framework focused on performance, strict typing, and developer ergonomics. It provides route handlers, dependency injection, middleware, WebSockets, SSE, OpenAPI generation, and a plugin system — all with tight Pydantic v2 integration and significantly lower overhead than FastAPI on high-throughput routes. Litestar is the choice for teams who want FastAPI's ergonomics with better performance and stricter type checking.
Install
pip install litestar
pip install litestar[full] # adds uvicorn, pydantic, attrs, msgspec, jinja2, etc.
pip install uvicorn # ASGI server
Output: (none — exits 0 on success)
Quick example
from litestar import Litestar, get
@get("/hello/{name:str}")
async def hello(name: str) -> dict:
return {"message": f"Hello, {name}!"}
app = Litestar([hello])
uvicorn main:app
Output: (none — exits 0 on success)
curl http://localhost:8000/hello/Alice
Output:
{"message":"Hello, Alice!"}
When / why to use it
- High-throughput JSON APIs where FastAPI's overhead is measurable — Litestar benchmarks 2–4× faster on simple routes.
- Strict type safety: Litestar validates return types, not just inputs, and raises at startup for type mismatches.
- Projects that need WebSockets, SSE, and HTTP/2 alongside REST in one framework.
- When you want batteries-included OpenAPI docs, built-in test client, and layered middleware without extra packages.
- Teams that want
msgspecorattrsmodels instead of Pydantic.
Common pitfalls
Return type annotation is mandatory — Litestar uses the handler's return type to select the serialiser and generate the OpenAPI schema. Omitting it or annotating
-> Nonewhen you return data raises a validation error at startup.
async defvsdef— Litestar runs sync handlers in a thread pool executor automatically, so both are supported. Useasync deffor I/O-bound handlers anddeffor CPU-bound ones.
ProvidevsDependency— useProvidein thedependencies={}dict on the router or app, andDependency()as the default value in the handler signature. Forgetting to register a provider raisesImproperlyConfiguredExceptionat startup, not at request time.
litestar.testing.TestClientprovides a synchronous test client that does not require a running server — ideal for pytest. UseAsyncTestClientfor async tests.
Annotate handler responses with
Response[T]to set status codes, headers, and cookies alongside the typed body.Response[MyModel]is both the OpenAPI schema and the runtime validator.
Route handlers
Litestar uses dedicated decorators per HTTP method: @get, @post, @put, @patch, @delete. All accept path, status_code, tags, and response_headers.
from litestar import Litestar, get, post, put, delete
from pydantic import BaseModel
from typing import Optional
class Item(BaseModel):
id: Optional[int] = None
name: str
price: float
ITEMS: dict[int, Item] = {}
_counter = 0
@post("/items", status_code=201)
async def create_item(data: Item) -> Item:
global _counter
_counter += 1
data.id = _counter
ITEMS[_counter] = data
return data
@get("/items")
async def list_items() -> list[Item]:
return list(ITEMS.values())
@get("/items/{item_id:int}")
async def get_item(item_id: int) -> Item:
from litestar.exceptions import NotFoundException
if item_id not in ITEMS:
raise NotFoundException(f"Item {item_id} not found")
return ITEMS[item_id]
@put("/items/{item_id:int}")
async def update_item(item_id: int, data: Item) -> Item:
from litestar.exceptions import NotFoundException
if item_id not in ITEMS:
raise NotFoundException(f"Item {item_id} not found")
data.id = item_id
ITEMS[item_id] = data
return data
@delete("/items/{item_id:int}", status_code=204)
async def delete_item(item_id: int) -> None:
ITEMS.pop(item_id, None)
app = Litestar([create_item, list_items, get_item, update_item, delete_item])
Path, query, and header parameters
Parameters are declared in the function signature. Litestar infers their source from context: path params match {name:type} in the route, everything else is a query param or body.
from litestar import get
from typing import Optional
@get("/search/{category:str}")
async def search(
category: str, # path parameter
query: str, # query parameter — ?query=...
page: int = 1, # query with default — ?page=2
limit: int = 20, # query with default
sort: Optional[str] = None, # optional query — ?sort=name
) -> dict:
return {
"category": category,
"query": query,
"page": page,
"limit": limit,
"sort": sort,
}
curl "http://localhost:8000/search/books?query=python&page=2&sort=title"
Output:
{"category":"books","query":"python","page":2,"limit":20,"sort":"title"}
Request body — Pydantic models
Annotate the handler parameter with a Pydantic BaseModel (or dataclass, msgspec.Struct, attrs) and Litestar deserialises and validates the JSON body automatically.
from litestar import post
from pydantic import BaseModel, field_validator
class CreateUserRequest(BaseModel):
username: str
email: str
age: int
@field_validator("age")
@classmethod
def check_age(cls, v: int) -> int:
if v < 0 or v > 150:
raise ValueError("Invalid age")
return v
class UserResponse(BaseModel):
id: int
username: str
email: str
@post("/users", status_code=201)
async def create_user(data: CreateUserRequest) -> UserResponse:
return UserResponse(id=1, username=data.username, email=data.email)
Dependency injection
Dependencies are declared in dependencies={} on the app, router, or individual handler. They can be async, can themselves declare dependencies, and are resolved per-request.
from litestar import Litestar, get
from litestar.di import Provide
from typing import Annotated
async def get_db_session():
"""Yields a mock DB session."""
yield {"connected": True}
async def get_current_user(db: Annotated[dict, Provide(get_db_session)]) -> dict:
return {"id": 1, "name": "Alice Dev", "role": "admin"}
@get("/profile")
async def profile(current_user: Annotated[dict, Provide(get_current_user)]) -> dict:
return current_user
app = Litestar(
[profile],
dependencies={"db": Provide(get_db_session)},
)
Middleware
Litestar supports standard ASGI middleware and its own AbstractMiddleware base class.
from litestar import Litestar, get
from litestar.middleware import AbstractMiddleware
from litestar.types import ASGIApp, Receive, Scope, Send
import time
class TimingMiddleware(AbstractMiddleware):
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
start = time.perf_counter()
await self.app(scope, receive, send)
elapsed = time.perf_counter() - start
print(f"{scope['path']} took {elapsed*1000:.1f}ms")
else:
await self.app(scope, receive, send)
@get("/ping")
async def ping() -> dict:
return {"status": "ok"}
app = Litestar([ping], middleware=[TimingMiddleware])
Exception handlers
from litestar import Litestar, get
from litestar.exceptions import HTTPException, NotFoundException
from litestar.types import Request
from litestar.response import Response
def not_found_handler(request: Request, exc: NotFoundException) -> Response:
return Response(
content={"detail": str(exc.detail), "path": request.url.path},
status_code=404,
)
def generic_error_handler(request: Request, exc: Exception) -> Response:
return Response(content={"error": "Internal server error"}, status_code=500)
@get("/items/{item_id:int}")
async def get_item(item_id: int) -> dict:
if item_id == 0:
raise NotFoundException("Item not found")
return {"id": item_id}
app = Litestar(
[get_item],
exception_handlers={
NotFoundException: not_found_handler,
500: generic_error_handler,
},
)
Routers — grouping routes
from litestar import Router, get, post, Litestar
@get("/{user_id:int}")
async def get_user(user_id: int) -> dict:
return {"id": user_id, "name": "Alice Dev"}
@post("/")
async def create_user(data: dict) -> dict:
return {"id": 99, **data}
user_router = Router(path="/users", route_handlers=[get_user, create_user])
app = Litestar(route_handlers=[user_router])
WebSockets
from litestar import Litestar, WebSocket, websocket
@websocket("/ws/{room:str}")
async def chat(socket: WebSocket, room: str) -> None:
await socket.accept()
await socket.send_text(f"Joined room: {room}")
try:
while True:
msg = await socket.receive_text()
await socket.send_text(f"Echo [{room}]: {msg}")
except Exception:
await socket.close()
app = Litestar([chat])
Testing
from litestar.testing import TestClient
from main import app
def test_create_item():
with TestClient(app=app) as client:
response = client.post("/items", json={"name": "Widget", "price": 9.99})
assert response.status_code == 201
body = response.json()
assert body["name"] == "Widget"
assert body["id"] is not None
def test_not_found():
with TestClient(app=app) as client:
response = client.get("/items/9999")
assert response.status_code == 404
def test_openapi_schema():
with TestClient(app=app) as client:
response = client.get("/schema/openapi.json")
assert response.status_code == 200
schema = response.json()
assert "/items" in schema["paths"]
OpenAPI and docs
Litestar generates OpenAPI 3.1 schemas automatically. Docs are served at /schema by default.
from litestar import Litestar
from litestar.openapi import OpenAPIConfig
app = Litestar(
route_handlers=[...],
openapi_config=OpenAPIConfig(
title="My API",
version="1.0.0",
description="A sample API built with Litestar",
contact={"name": "Alice Dev", "email": "alice@example.com"},
),
)
# Docs at: http://localhost:8000/schema (Swagger UI)
# JSON at: http://localhost:8000/schema/openapi.json
Real-world recipes
A handful of end-to-end Litestar features wired together — the patterns most production APIs settle on.
1. DTO-driven CRUD with explicit exclude
from dataclasses import dataclass
from litestar import Litestar, get, post
from litestar.dto import DataclassDTO, DTOConfig
@dataclass
class User:
id: int
email: str
password_hash: str
is_admin: bool
class UserReadDTO(DataclassDTO[User]):
"""Strips sensitive fields from the response."""
config = DTOConfig(exclude={"password_hash"})
class UserCreateDTO(DataclassDTO[User]):
"""Allows only the fields the client may set on create."""
config = DTOConfig(include={"email"})
USERS: dict[int, User] = {}
_next = 0
@post("/users", dto=UserCreateDTO, return_dto=UserReadDTO)
async def create_user(data: User) -> User:
global _next
_next += 1
u = User(id=_next, email=data.email, password_hash="hash", is_admin=False)
USERS[_next] = u
return u
@get("/users/{user_id:int}", return_dto=UserReadDTO)
async def get_user(user_id: int) -> User:
return USERS[user_id]
app = Litestar([create_user, get_user])
Output: (none — exits 0 on success)
2. msgspec models for low-overhead JSON
import msgspec
from litestar import Litestar, post
class Item(msgspec.Struct):
name: str
price: float
in_stock: bool = True
@post("/items")
async def create(data: Item) -> Item:
# msgspec encoding is ~5–10× faster than pydantic for this hot path
return data
app = Litestar([create])
3. Layered middleware — auth + rate-limit + CORS
from litestar import Litestar, get
from litestar.config.cors import CORSConfig
from litestar.middleware.rate_limit import RateLimitConfig
from litestar.middleware.session.client_side import ClientSideSessionBackend, ClientSideSessionConfig
from litestar.exceptions import NotAuthorizedException
async def require_token(request) -> None:
token = request.headers.get("X-API-Key")
if token != "secret":
raise NotAuthorizedException("Invalid API key")
@get("/private", before_request=require_token)
async def private() -> dict:
return {"ok": True}
app = Litestar(
[private],
cors_config=CORSConfig(allow_origins=["https://app.example.com"]),
middleware=[
RateLimitConfig(rate_limit=("minute", 60)).middleware,
],
)
4. Dependency-injected DB session with SQLAlchemy
from litestar import Litestar, get
from litestar.di import Provide
from typing import Annotated, AsyncIterator
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
engine = create_async_engine("postgresql+asyncpg://localhost/app")
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def db_session() -> AsyncIterator[AsyncSession]:
async with SessionLocal() as session:
yield session
@get("/users/{user_id:int}")
async def get_user(
user_id: int,
db: Annotated[AsyncSession, Provide(db_session)],
) -> dict:
row = (await db.execute("SELECT email FROM users WHERE id=:i", {"i": user_id})).first()
return {"id": user_id, "email": row.email}
app = Litestar([get_user], dependencies={"db": Provide(db_session)})
5. WebSocket chatroom with shared state
from litestar import Litestar, WebSocket, websocket
from collections import defaultdict
ROOMS: dict[str, set[WebSocket]] = defaultdict(set)
@websocket("/chat/{room:str}")
async def chatroom(socket: WebSocket, room: str) -> None:
await socket.accept()
ROOMS[room].add(socket)
try:
async for msg in socket.iter_text():
for peer in ROOMS[room]:
if peer is not socket:
await peer.send_text(msg)
finally:
ROOMS[room].discard(socket)
await socket.close()
app = Litestar([chatroom])
6. SSE streaming response
import asyncio
from litestar import Litestar, get
from litestar.response import ServerSentEvent
@get("/stream")
async def stream() -> ServerSentEvent:
async def gen():
for i in range(10):
yield {"event": "tick", "data": f"chunk {i}"}
await asyncio.sleep(0.5)
return ServerSentEvent(content=gen())
app = Litestar([stream])
7. File upload with form data
from litestar import Litestar, post
from litestar.datastructures import UploadFile
from litestar.enums import RequestEncodingType
from litestar.params import Body
from typing import Annotated
@post("/upload")
async def upload(
data: Annotated[UploadFile, Body(media_type=RequestEncodingType.MULTI_PART)],
) -> dict:
contents = await data.read()
return {"filename": data.filename, "size": len(contents)}
app = Litestar([upload])
Production deployment
Litestar is a pure ASGI app — any ASGI server works. Production deployments most commonly use Granian (Rust-based, fastest) or Uvicorn + multiple workers, behind a reverse proxy.
# Granian — fastest ASGI server, single binary
pip install granian
granian --interface asgi --workers 4 --port 8000 main:app
# Uvicorn — most widely deployed
pip install "uvicorn[standard]"
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
# Hypercorn — HTTP/2 + HTTP/3 support
hypercorn -k uvloop main:app --bind 0.0.0.0:8000 --workers 4
Output: (none — exits 0 on success)
nginx.conf — reverse proxy fragment:
upstream litestar {
least_conn;
server 127.0.0.1:8000;
server 127.0.0.1:8001;
}
server {
listen 443 ssl http2;
server_name api.example.com;
location / {
proxy_pass http://litestar;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
}
}
Dockerfile template:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["granian", "--interface", "asgi", "--workers", "4", "--host", "0.0.0.0", "--port", "8000", "main:app"]
Production checklist:
- Pick Granian for new deploys — typically 2–3× the throughput of Uvicorn for pure-ASGI workloads.
- Set
workers = (2 × CPU) + 1as a starting point; benchmark and adjust. - Always run behind a reverse proxy (nginx, Caddy, Traefik) for TLS termination, gzip, and connection management.
- Pin
litestarto a minor version — the framework is stable but moves fast. - Enable structured logging via
litestar.config.logging.LoggingConfig— JSON logs play well with Datadog/Loki. - Set
app.debug=Falsein prod; debug mode leaks stack traces in 500 responses.
Performance tuning
Litestar's hot path is already significantly faster than FastAPI. Further tuning focuses on serialisation choice, dependency caching, and worker concurrency.
# 1. Use msgspec over pydantic for hot paths
import msgspec
class FastModel(msgspec.Struct): # ~5–10× faster encode/decode than pydantic
name: str
# 2. Cache expensive dependencies with sync_to_thread
from litestar.di import Provide
def expensive_setup() -> dict:
return load_heavy_config()
# use_cache=True keeps the result for the lifetime of the app
app = Litestar(
[...],
dependencies={"config": Provide(expensive_setup, use_cache=True, sync_to_thread=False)},
)
# 3. async def handlers — Litestar runs sync def in a threadpool
@get("/fast")
async def fast() -> dict: # ✅ runs on the event loop directly
return {"ok": True}
@get("/slow")
def slow() -> dict: # ⚠️ runs in a threadpool — extra hop
return {"ok": True}
# 4. Compression middleware
from litestar.config.compression import CompressionConfig
app = Litestar(
[...],
compression_config=CompressionConfig(backend="gzip", minimum_size=500),
)
Output: (none — exits 0 on success)
Tuning checklist:
- Prefer
msgspec.Structover Pydantic on hot paths — serialisation can dominate response time. - Use
async defunless the handler genuinely blocks the event loop on a CPU task. - Cache config-like dependencies with
use_cache=True. - Enable gzip/brotli compression for responses > 500 bytes.
- Disable OpenAPI in production load-test runs if you don't need the schema endpoint — saves startup time on cold containers.
- Use
uvloop(auto-installed withlitestar[standard]) — 2× faster event loop than the default asyncio one.
Testing patterns
Litestar's testing story is one of its strengths — TestClient is httpx-based and works without a running server.
from litestar.testing import TestClient, AsyncTestClient
from main import app
# 1. Synchronous client — most pytest-friendly
def test_create_item():
with TestClient(app=app) as client:
r = client.post("/items", json={"name": "Widget", "price": 9.99})
assert r.status_code == 201
assert r.json()["name"] == "Widget"
# 2. Async client — for async fixtures and dependencies
import pytest
@pytest.mark.asyncio
async def test_async_route():
async with AsyncTestClient(app=app) as client:
r = await client.get("/items")
assert r.status_code == 200
# 3. Override dependencies in tests
from litestar.di import Provide
async def fake_db_session():
yield {"test": True}
def test_with_fake_db():
app_with_fake = Litestar(
route_handlers=app.route_handlers,
dependencies={"db": Provide(fake_db_session)},
)
with TestClient(app=app_with_fake) as client:
r = client.get("/profile")
assert r.status_code == 200
# 4. WebSocket testing
def test_chat_websocket():
with TestClient(app=app) as client:
with client.websocket_connect("/chat/room1") as ws:
ws.send_text("hello")
assert ws.receive_text() == "Echo [room1]: hello"
# 5. Assert on the OpenAPI schema
def test_schema_has_all_routes():
with TestClient(app=app) as client:
schema = client.get("/schema/openapi.json").json()
assert "/items" in schema["paths"]
assert "post" in schema["paths"]["/items"]
Output: (none — exits 0 on success)
Patterns:
- Use
TestClientas a context manager so lifespan startup/shutdown events fire. - Override dependencies per test by constructing a fresh
Litestarwith the same handlers and newdependencies={}. - Mock external HTTP with
respx(httpx-native) — most Litestar codebases usehttpxfor outbound calls. - Fixture the TestClient at session scope when possible; lifespan startup is non-trivial.
Migration from Starlite
Litestar was renamed from Starlite in 2023 (the project, not the company; Starlite Aerospace was the trademark blocker). The 1.x → 2.x bump also introduced breaking API changes.
| Concept | Starlite 1.x | Litestar 2.x |
|---|---|---|
| Module | import starlite | import litestar |
| App class | Starlite(...) | Litestar(...) |
| Test client | from starlite.testing import TestClient | from litestar.testing import TestClient |
| DTO | DTOFactory | DataclassDTO[T] / PydanticDTO[T] / MsgspecDTO[T] |
| Plugins | PluginProtocol | InitPlugin, SerializationPlugin, OpenAPISchemaPlugin |
| State | app.state.X | app.state["X"] (State is a mapping) |
| Provide | from starlite.di import Provide | from litestar.di import Provide |
Migration steps:
- Run
s/starlite/litestar/gacross imports and config. - Replace
DTOFactorywith the new generic DTOs —DataclassDTO[User],PydanticDTO[User], orMsgspecDTO[User]. - Update plugin classes to the new split (Init / Serialization / OpenAPI).
- Check return type annotations — Litestar 2.x is stricter;
-> Nonewhile returning data raises at startup. - Re-pin
litestar[full]extra — the bundled deps changed (msgspec is now default).
Litestar vs FastAPI
Litestar and FastAPI cover overlapping ground. The honest framing:
| Dimension | FastAPI | Litestar |
|---|---|---|
| Stars / community | Larger | Smaller but active |
| Tutorial coverage | Massive | Solid, growing |
| Throughput on simple JSON routes | Baseline | ~2–4× higher |
| Startup time validation | Lazy (request-time) | Eager (startup) |
| DI system | Simple, function-based | Layered (app/router/handler) |
| DTOs | Manual (Pydantic models) | First-class DTO abstraction |
| WebSockets / SSE | Available | First-class |
| Pydantic / msgspec / attrs | Pydantic-centric | All three are first-class |
| Plugin system | Minimal | Multi-protocol plugin hooks |
| Built-in middleware | Less | Rate limit, compression, sessions, CORS |
Pick FastAPI when: you want the deepest tutorial coverage, your team already knows it, or you need the widest ecosystem of third-party plugins.
Pick Litestar when: throughput matters, you want stricter typing, you want msgspec/attrs/dataclass support out of the box, or you need first-class DTOs.
Ecosystem integrations
Litestar's plugin system makes it integration-friendly. The major first-party plugins:
| Package | Purpose |
|---|---|
| advanced-alchemy | SQLAlchemy 2 plugin: async sessions, repositories, lifespan |
| piccolo-orm | Piccolo ORM plugin |
| msgspec | Default serializer; ~10× faster than json on the hot path |
| attrs | Use @attrs.define classes as DTOs |
| OpenAPI plugins | Custom schema generation, Swagger / Redoc / Stoplight UIs |
| Jinja / Mako | First-class HTML templating |
| Granian | Recommended ASGI server in production |
| Uvicorn | Most widely-deployed ASGI server |
Patterns & idioms
- Controllers for resource grouping.
class UserController(Controller): path = "/users"collects related handlers — cleaner than top-level decorators when the API grows. - DTOs over hand-written response shapes. Define
UserReadDTOandUserCreateDTOfrom the same dataclass — the schema and validation flow from one source. - Layered dependencies. App → Router → Controller → Handler, each with
dependencies={}. Handler-level wins. - Plugins for cross-cutting concerns. Database lifespan, OpenAPI customisation, JWT auth all fit the plugin protocol.
Response[T]for custom headers / status. ReturningResponse(value, status_code=202, headers={"X-Job-Id": "..."})keeps the type and the metadata together.before_request/after_requesthooks per handler for auth checks and per-request logging.- Lifespan with
on_startup/on_shutdownfor DB pools and async clients.
Troubleshooting common errors
| Error | Cause | Fix |
|---|---|---|
ImproperlyConfiguredException: ...has no return annotation | Handler missing return type | Add -> dict / -> Model / -> None |
ImproperlyConfiguredException: Dependency 'X' not found | Provider not registered | Add to dependencies={"X": Provide(...)} on app, router, or handler |
ValidationException on incoming body | Pydantic/msgspec rejected the payload | Inspect the response body — it includes per-field errors |
| OpenAPI schema missing routes | Routes not passed to Litestar(...) | Confirm all handlers in route_handlers=[...] |
WebSocket 1006 abnormal closure behind nginx | Proxy not forwarding Upgrade | Add proxy_set_header Upgrade $http_upgrade |
| Tests hang at app startup | Lifespan startup blocking | Use TestClient as a context manager, not bare init |
Worker timeout in uvicorn under load | Sync handler blocking the loop | Switch to async def or add sync_to_thread=True on the dep |
RuntimeError: This event loop is already running | Calling asyncio.run inside a handler | Use the existing loop with await or asyncio.get_event_loop() |
405 Method Not Allowed on a known route | Route defined with @get but request is POST | Add the appropriate decorator |
When NOT to use this
Litestar is a strong default, but the wider Python web ecosystem still has cases where another choice wins.
- You need the largest Stack Overflow / tutorial coverage. FastAPI has more material online; for small teams onboarding fast, that matters.
- You're building a Django-style monolith with templating + admin + ORM. Django ships those out of the box.
- You're writing a tiny one-file API. Flask is still the simplest single-file framework.
- You need Python 3.9 compatibility. Litestar requires 3.9+ on the current major but moves fast — verify against your floor.
- You're standing up a static-only site. Use a static-site generator (Astro, Eleventy); a web framework is overkill.
Quick reference
| Task | Code |
|---|---|
| GET handler | @get("/path") async def fn() -> T: |
| POST handler | @post("/path") async def fn(data: Model) -> T: |
| Path param | @get("/items/{id:int}") async def fn(id: int) |
| Query param | async def fn(q: str, page: int = 1) |
| Dependency | @get(...) async def fn(dep: Annotated[T, Provide(factory)]) |
| Global dep | Litestar(dependencies={"key": Provide(factory)}) |
| Middleware | Litestar(middleware=[MyMiddleware]) |
| Exception handler | Litestar(exception_handlers={404: handler}) |
| Router | Router(path="/prefix", route_handlers=[...]) |
| WebSocket | @websocket("/ws") async def fn(socket: WebSocket) |
| Test client | TestClient(app=app) |
| Not found | raise NotFoundException("msg") |
| OpenAPI config | Litestar(openapi_config=OpenAPIConfig(...)) |
| Docs URL | /schema (Swagger), /schema/openapi.json (raw) |