skip to content

litestar — High-Performance ASGI Framework

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.

7 min read 17 snippets deep dive

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 msgspec or attrs models instead of Pydantic.

Common pitfalls#

[!WARNING] 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.

[!WARNING] 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.

[!WARNING] 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.

[!TIP] litestar.testing.TestClient provides a synchronous test client that does not require a running server — ideal for pytest. Use AsyncTestClient for async tests.

[!TIP] 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

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)