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

bash
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

python
from litestar import Litestar, get

@get("/hello/{name:str}")
async def hello(name: str) -> dict:
    return {"message": f"Hello, {name}!"}

app = Litestar([hello])
bash
uvicorn main:app

Output: (none — exits 0 on success)

bash
curl http://localhost:8000/hello/Alice

Output:

text
{"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 msgspec or attrs models 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 -> None when you return data raises a validation error at startup.

async def vs def — Litestar runs sync handlers in a thread pool executor automatically, so both are supported. Use async def for I/O-bound handlers and def for CPU-bound ones.

Provide vs Dependency — use Provide in the dependencies={} dict on the router or app, and Dependency() as the default value in the handler signature. Forgetting to register a provider raises ImproperlyConfiguredException at startup, not at request time.

litestar.testing.TestClient provides a synchronous test client that does not require a running server — ideal for pytest. Use AsyncTestClient for 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.

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

python
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,
    }
bash
curl "http://localhost:8000/search/books?query=python&page=2&sort=title"

Output:

json
{"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.

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

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

python
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

python
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

python
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

python
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

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

python
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

python
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

python
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

python
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

python
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

python
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

python
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

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

bash
# 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:

nginx
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:

dockerfile
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:

  1. Pick Granian for new deploys — typically 2–3× the throughput of Uvicorn for pure-ASGI workloads.
  2. Set workers = (2 × CPU) + 1 as a starting point; benchmark and adjust.
  3. Always run behind a reverse proxy (nginx, Caddy, Traefik) for TLS termination, gzip, and connection management.
  4. Pin litestar to a minor version — the framework is stable but moves fast.
  5. Enable structured logging via litestar.config.logging.LoggingConfig — JSON logs play well with Datadog/Loki.
  6. Set app.debug=False in 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.

python
# 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:

  1. Prefer msgspec.Struct over Pydantic on hot paths — serialisation can dominate response time.
  2. Use async def unless the handler genuinely blocks the event loop on a CPU task.
  3. Cache config-like dependencies with use_cache=True.
  4. Enable gzip/brotli compression for responses > 500 bytes.
  5. Disable OpenAPI in production load-test runs if you don't need the schema endpoint — saves startup time on cold containers.
  6. Use uvloop (auto-installed with litestar[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.

python
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 TestClient as a context manager so lifespan startup/shutdown events fire.
  • Override dependencies per test by constructing a fresh Litestar with the same handlers and new dependencies={}.
  • Mock external HTTP with respx (httpx-native) — most Litestar codebases use httpx for 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.

ConceptStarlite 1.xLitestar 2.x
Moduleimport starliteimport litestar
App classStarlite(...)Litestar(...)
Test clientfrom starlite.testing import TestClientfrom litestar.testing import TestClient
DTODTOFactoryDataclassDTO[T] / PydanticDTO[T] / MsgspecDTO[T]
PluginsPluginProtocolInitPlugin, SerializationPlugin, OpenAPISchemaPlugin
Stateapp.state.Xapp.state["X"] (State is a mapping)
Providefrom starlite.di import Providefrom litestar.di import Provide

Migration steps:

  1. Run s/starlite/litestar/g across imports and config.
  2. Replace DTOFactory with the new generic DTOsDataclassDTO[User], PydanticDTO[User], or MsgspecDTO[User].
  3. Update plugin classes to the new split (Init / Serialization / OpenAPI).
  4. Check return type annotations — Litestar 2.x is stricter; -> None while returning data raises at startup.
  5. 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:

DimensionFastAPILitestar
Stars / communityLargerSmaller but active
Tutorial coverageMassiveSolid, growing
Throughput on simple JSON routesBaseline~2–4× higher
Startup time validationLazy (request-time)Eager (startup)
DI systemSimple, function-basedLayered (app/router/handler)
DTOsManual (Pydantic models)First-class DTO abstraction
WebSockets / SSEAvailableFirst-class
Pydantic / msgspec / attrsPydantic-centricAll three are first-class
Plugin systemMinimalMulti-protocol plugin hooks
Built-in middlewareLessRate 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:

PackagePurpose
advanced-alchemySQLAlchemy 2 plugin: async sessions, repositories, lifespan
piccolo-ormPiccolo ORM plugin
msgspecDefault serializer; ~10× faster than json on the hot path
attrsUse @attrs.define classes as DTOs
OpenAPI pluginsCustom schema generation, Swagger / Redoc / Stoplight UIs
Jinja / MakoFirst-class HTML templating
GranianRecommended ASGI server in production
UvicornMost 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 UserReadDTO and UserCreateDTO from 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. Returning Response(value, status_code=202, headers={"X-Job-Id": "..."}) keeps the type and the metadata together.
  • before_request / after_request hooks per handler for auth checks and per-request logging.
  • Lifespan with on_startup / on_shutdown for DB pools and async clients.

Troubleshooting common errors

ErrorCauseFix
ImproperlyConfiguredException: ...has no return annotationHandler missing return typeAdd -> dict / -> Model / -> None
ImproperlyConfiguredException: Dependency 'X' not foundProvider not registeredAdd to dependencies={"X": Provide(...)} on app, router, or handler
ValidationException on incoming bodyPydantic/msgspec rejected the payloadInspect the response body — it includes per-field errors
OpenAPI schema missing routesRoutes not passed to Litestar(...)Confirm all handlers in route_handlers=[...]
WebSocket 1006 abnormal closure behind nginxProxy not forwarding UpgradeAdd proxy_set_header Upgrade $http_upgrade
Tests hang at app startupLifespan startup blockingUse TestClient as a context manager, not bare init
Worker timeout in uvicorn under loadSync handler blocking the loopSwitch to async def or add sync_to_thread=True on the dep
RuntimeError: This event loop is already runningCalling asyncio.run inside a handlerUse the existing loop with await or asyncio.get_event_loop()
405 Method Not Allowed on a known routeRoute defined with @get but request is POSTAdd 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

TaskCode
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 paramasync def fn(q: str, page: int = 1)
Dependency@get(...) async def fn(dep: Annotated[T, Provide(factory)])
Global depLitestar(dependencies={"key": Provide(factory)})
MiddlewareLitestar(middleware=[MyMiddleware])
Exception handlerLitestar(exception_handlers={404: handler})
RouterRouter(path="/prefix", route_handlers=[...])
WebSocket@websocket("/ws") async def fn(socket: WebSocket)
Test clientTestClient(app=app)
Not foundraise NotFoundException("msg")
OpenAPI configLitestar(openapi_config=OpenAPIConfig(...))
Docs URL/schema (Swagger), /schema/openapi.json (raw)