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#
[!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
-> Nonewhen you return data raises a validation error at startup.
[!WARNING]
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.
[!WARNING]
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.
[!TIP]
litestar.testing.TestClientprovides a synchronous test client that does not require a running server — ideal for pytest. UseAsyncTestClientfor 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#
| 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) |