Fix: Litestar Not Working — Dependency Injection, msgspec Validation, and Controller Setup
Quick Answer
How to fix Litestar errors — Starlite to Litestar migration, Dependency injection scope, controller route not found, msgspec validation differs from Pydantic, lifespan handler setup, and OpenAPI generation.
The Error
You install Litestar and old Starlite tutorials don’t work:
from starlite import Starlite, get # ImportError — package renamedOr dependency injection scopes confuse:
from litestar import get
from litestar.di import Provide
async def get_db():
return Database()
@get("/users", dependencies={"db": Provide(get_db)})
async def list_users(db: Database):
# Same Database instance across all requests — should be per-request
...Or msgspec-based validation rejects valid input:
from litestar import post
class UserIn(msgspec.Struct):
age: int
@post("/users")
async def create_user(data: UserIn) -> dict:
...
# POST {"age": "30"}
# 400 Bad Request — msgspec is strict, doesn't auto-coerceOr controller routes don’t register:
from litestar import Controller, get
class UserController(Controller):
path = "/users"
@get()
async def list_users(self) -> list:
...
# Forgot to add UserController to route_handlers — silently missingOr lifespan handlers don’t fire:
async def startup():
print("starting") # Never prints
app = Litestar(route_handlers=[...]) # No on_startup specifiedLitestar (renamed from Starlite in 2023) is the modern ASGI framework — fastest validation in Python via msgspec, controller-based routing, layered dependency injection. It competes with FastAPI but with stricter typing and better performance. The features differ enough from FastAPI that experienced FastAPI developers need to relearn several patterns. This guide covers each common issue.
Why This Happens
Litestar uses msgspec for validation by default (Pydantic optional via plugins) — strict typing, no auto-coercion. The dependency injection system has explicit scopes (request, lifecycle) — different from FastAPI’s lifecycle-agnostic Depends. Controllers and routers are first-class organizational units — different from FastAPI’s flat router model.
The Starlite → Litestar rename in October 2023 broke every import. Old tutorials still appear in search results and confuse newcomers.
Fix 1: Starlite → Litestar Migration
# OLD — Starlite (broken in Litestar)
from starlite import Starlite, get, post
from starlite.di import Provide
# NEW — Litestar
from litestar import Litestar, get, post
from litestar.di import ProvidePackage install:
# OLD
pip install starlite
# NEW
pip install litestarImport migration:
| Starlite | Litestar |
|---|---|
from starlite import ... | from litestar import ... |
from starlite.di import Provide | from litestar.di import Provide |
from starlite.exceptions import ... | from litestar.exceptions import ... |
from starlite.contrib.sqlalchemy import ... | from litestar.contrib.sqlalchemy import ... |
from starlite.testing import TestClient | from litestar.testing import TestClient |
Common Mistake: Following a tutorial that uses from starlite import ... — the package was renamed; the API is mostly compatible but imports break completely. Always check if a tutorial uses starlite or litestar — the dates matter (October 2023 split).
Fix 2: Basic Litestar App
from litestar import Litestar, get, post
from msgspec import Struct
from typing import Annotated
class User(Struct):
id: int
name: str
email: str
class UserCreate(Struct):
name: str
email: str
@get("/users")
async def list_users() -> list[User]:
return [User(id=1, name="Alice", email="[email protected]")]
@get("/users/{user_id:int}")
async def get_user(user_id: int) -> User:
return User(id=user_id, name="Alice", email="[email protected]")
@post("/users")
async def create_user(data: UserCreate) -> User:
return User(id=42, name=data.name, email=data.email)
app = Litestar(route_handlers=[list_users, get_user, create_user])Run:
litestar run
# Or with hot reload for dev
litestar run --reloadPath parameters with type hints:
@get("/users/{user_id:int}") # int (default if omitted)
@get("/users/{user_id:uuid}") # UUID
@get("/users/{name:str}") # stringQuery parameters via function args:
@get("/search")
async def search(query: str, limit: int = 10, offset: int = 0) -> list:
return [...]
# Usage: /search?query=alice&limit=20Common Mistake: Defining a handler but forgetting to add it to route_handlers=[...]. Litestar doesn’t auto-discover handlers — every endpoint must be explicitly registered. Without registration, the URL returns 404.
Fix 3: Controllers — Class-Based Routing
from litestar import Controller, get, post, patch, delete
class UserController(Controller):
path = "/users"
@get()
async def list_users(self) -> list[User]:
return [...]
@get("/{user_id:int}")
async def get_user(self, user_id: int) -> User:
return User(...)
@post()
async def create_user(self, data: UserCreate) -> User:
return User(...)
@patch("/{user_id:int}")
async def update_user(self, user_id: int, data: UserUpdate) -> User:
return User(...)
@delete("/{user_id:int}")
async def delete_user(self, user_id: int) -> None:
...
app = Litestar(route_handlers=[UserController])The controller groups related routes under a path prefix. Routes inside the class inherit the controller’s path — /users, /users/{user_id}, etc.
Tags for OpenAPI grouping:
class UserController(Controller):
path = "/users"
tags = ["users"]
@get()
async def list_users(self) -> list[User]: ...Per-controller dependencies and middleware:
class AdminController(Controller):
path = "/admin"
dependencies = {"current_user": Provide(get_current_user)}
guards = [admin_guard] # Run before every handler
middleware = [audit_middleware]Pro Tip: Use Controllers for any group of >2 related routes. They give you a clean place to define shared dependencies, guards, and middleware. FastAPI users often default to flat functions — in Litestar, controllers are idiomatic and reduce boilerplate.
Fix 4: Dependency Injection Scopes
from litestar import get, Litestar
from litestar.di import Provide
async def get_db():
db = Database()
try:
yield db
finally:
await db.close()
@get("/users", dependencies={"db": Provide(get_db)})
async def list_users(db: Database) -> list[User]:
return await db.fetch_all_users()
app = Litestar(route_handlers=[list_users])Dependency scopes — where you declare the dependency determines its scope:
| Where declared | Scope |
|---|---|
| Handler-level | Per-request, this handler only |
| Controller-level | Per-request, all controller handlers |
| Router-level | Per-request, all routes in router |
| App-level | Per-request, every route in the app |
App-level dependencies:
app = Litestar(
route_handlers=[...],
dependencies={
"db": Provide(get_db),
"settings": Provide(get_settings, sync_to_thread=False),
},
)sync_to_thread parameter — Litestar runs sync dependencies in a thread by default to avoid blocking the event loop. For dependencies that are fast and sync (config, constants), set sync_to_thread=False for speed:
def get_config() -> Config:
return Config(...)
# Wraps in a thread (default) — slight overhead but safe
Provide(get_config)
# Inline execution — faster but blocks if slow
Provide(get_config, sync_to_thread=False)Common Mistake: Using sync_to_thread=False on a sync dependency that does I/O (file read, network call). It blocks the event loop for the duration — kills server concurrency. Only set False for truly fast pure-compute deps.
Dependency caching within a request:
@get("/users")
async def list_users(
db: Database,
current_user: User, # Same User instance even if dep is requested twice
) -> list:
return await db.users.all()Litestar caches dependency resolution within a single request — calling Provide(get_current_user) twice in the same request returns the same instance.
For FastAPI dependency patterns that overlap with Litestar’s, see FastAPI dependency injection error.
Fix 5: Request Bodies and Validation
Litestar uses msgspec for validation. Defaults are strict:
from msgspec import Struct
class UserCreate(Struct):
name: str
age: int
email: str
@post("/users")
async def create_user(data: UserCreate) -> User:
return User(id=42, name=data.name, age=data.age, email=data.email)Strict by default — no auto-coercion:
curl -X POST http://localhost:8000/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "age": "30", "email": "[email protected]"}'
# 400 Bad Request — age is string, expected intFor Pydantic-style validation, install the Pydantic plugin:
pip install "litestar[pydantic]"from pydantic import BaseModel
from litestar import post
class UserCreate(BaseModel):
name: str
age: int
email: str
@post("/users")
async def create_user(data: UserCreate) -> User:
...Litestar supports both — use msgspec for performance, Pydantic for compatibility with existing models.
Constrained types with msgspec:
from typing import Annotated
import msgspec
class UserCreate(msgspec.Struct):
name: Annotated[str, msgspec.Meta(min_length=1, max_length=100)]
age: Annotated[int, msgspec.Meta(ge=0, le=150)]
email: Annotated[str, msgspec.Meta(pattern=r".+@.+")]For msgspec-specific validation patterns, see msgspec not working.
Fix 6: Lifespan and Startup/Shutdown
from contextlib import asynccontextmanager
from litestar import Litestar
@asynccontextmanager
async def lifespan(app: Litestar):
# Startup
db = await create_db_pool()
app.state.db = db
yield
# Shutdown
await db.close()
app = Litestar(route_handlers=[...], lifespan=[lifespan])Or use on_startup / on_shutdown callbacks:
async def startup_db():
db = await create_db_pool()
return db # Available as app.state.db
async def shutdown_db(app):
await app.state.db.close()
app = Litestar(
route_handlers=[...],
on_startup=[startup_db],
on_shutdown=[shutdown_db],
)Common Mistake: Defining a startup function but forgetting to register it with on_startup=[startup]. The function is never called — your app starts without initialization, and queries fail with confusing errors. Always check that startup callbacks are wired in.
App state is the canonical place for shared resources:
from litestar import get
@get("/users")
async def list_users(state) -> list:
db = state.db
return await db.fetch_users()state is injected automatically — access app.state attributes from any handler.
Fix 7: OpenAPI and Swagger UI
OpenAPI is auto-generated from your handlers and types. Access it at:
/schema— JSON schema/schema/swagger— Swagger UI/schema/redoc— ReDoc UI/schema/elements— Stoplight Elements UI
Customize:
from litestar import Litestar
from litestar.openapi import OpenAPIConfig
from litestar.openapi.spec.tag import Tag
app = Litestar(
route_handlers=[...],
openapi_config=OpenAPIConfig(
title="My API",
version="1.0.0",
description="API for managing users",
tags=[
Tag(name="users", description="User operations"),
Tag(name="admin", description="Admin operations"),
],
),
)Exclude routes from OpenAPI:
@get("/internal/health", include_in_schema=False)
async def health() -> dict:
return {"status": "ok"}Useful for internal endpoints (health checks, metrics) that shouldn’t appear in public docs.
Fix 8: Testing with TestClient
from litestar.testing import TestClient
def test_list_users():
with TestClient(app=app) as client:
response = client.get("/users")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["name"] == "Alice"
def test_create_user():
with TestClient(app=app) as client:
response = client.post(
"/users",
json={"name": "Bob", "age": 25, "email": "[email protected]"},
)
assert response.status_code == 201Async test client (for testing async code with proper event loop):
import pytest
from litestar.testing import AsyncTestClient
@pytest.mark.asyncio
async def test_async():
async with AsyncTestClient(app=app) as client:
response = await client.get("/users")
assert response.status_code == 200Override dependencies in tests:
def get_test_db() -> TestDatabase:
return TestDatabase()
def test_with_override():
app.dependencies["db"] = Provide(get_test_db)
with TestClient(app=app) as client:
# Now uses TestDatabase instead of real Database
...For pytest async fixture patterns with Litestar tests, see pytest fixture not found.
Still Not Working?
Litestar vs FastAPI
- Litestar — Faster validation via msgspec, more opinionated structure (controllers), stricter typing.
- FastAPI — Larger ecosystem, more tutorials, Pydantic-first. See FastAPI dependency injection error.
Both are excellent. Pick Litestar if performance and strictness matter; pick FastAPI if you need the broader ecosystem or already use Pydantic heavily.
Guards (Authorization)
from litestar import Request
from litestar.connection import ASGIConnection
from litestar.handlers.base import BaseRouteHandler
from litestar.exceptions import NotAuthorizedException
def is_admin(connection: ASGIConnection, route_handler: BaseRouteHandler) -> None:
user = connection.user
if not user or not user.is_admin:
raise NotAuthorizedException()
@get("/admin", guards=[is_admin])
async def admin_page() -> str:
return "secret"Guards run before handlers — raise NotAuthorizedException (403) or PermissionDeniedException (401) to block access.
Middleware
from litestar.middleware import DefineMiddleware
from litestar.types import Message, Receive, Scope, Send
class TimingMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
import time
start = time.perf_counter()
await self.app(scope, receive, send)
elapsed = time.perf_counter() - start
print(f"Request took {elapsed:.2f}s")
app = Litestar(
route_handlers=[...],
middleware=[DefineMiddleware(TimingMiddleware)],
)Database Integration
Litestar has first-class SQLAlchemy support via litestar.contrib.sqlalchemy:
from litestar.contrib.sqlalchemy.plugins import SQLAlchemyAsyncConfig, SQLAlchemyPlugin
sqla_config = SQLAlchemyAsyncConfig(
connection_string="postgresql+asyncpg://user:pass@localhost/db",
create_all=True,
)
app = Litestar(
route_handlers=[...],
plugins=[SQLAlchemyPlugin(config=sqla_config)],
)For SQLAlchemy patterns that work with Litestar, see SQLAlchemy not working. For asyncpg connection pool issues, see asyncpg not working.
Background Tasks
from litestar import get
from litestar.background_tasks import BackgroundTask
async def send_email(to: str, body: str):
# Email sending logic
...
@get("/notify", background=BackgroundTask(send_email, "[email protected]", "Hello"))
async def notify() -> dict:
return {"sent": True}The background task runs after the response is sent — handler returns quickly, task continues in the background.
For Uvicorn / Gunicorn deployment patterns that apply to Litestar, see Uvicorn not working and Gunicorn not working.
Deployment
Litestar runs on any ASGI server:
# Uvicorn
uvicorn app:app --host 0.0.0.0 --port 8000
# Hypercorn (HTTP/2 support)
hypercorn app:app --bind 0.0.0.0:8000
# Granian (Rust ASGI server, fast)
granian --interface asgi app:appFor production, run multiple workers via Gunicorn + UvicornWorker, same as FastAPI patterns.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: msgspec Not Working — Struct Definition, Type Validation, and JSON/MessagePack Encoding
How to fix msgspec errors — Struct field type not supported, ValidationError on decode, msgspec vs Pydantic differences, custom type hooks, frozen Struct mutation, and JSON Schema generation.
Fix: Uvicorn Not Working — Worker Errors, Reload Issues, and Production Deployment
How to fix Uvicorn errors — Address already in use port binding, reload not detecting changes, SSL certificate errors, worker class with gunicorn, WebSocket disconnect, graceful shutdown, and proxy headers behind nginx.
Fix: joblib Not Working — Parallel Backends, Memory Cache, and Pickling Errors
How to fix joblib errors — Parallel n_jobs slower than expected, Memory cache miss, backend loky vs threading vs multiprocessing, pickling lambda not supported, dump load file size, and pytest interference.
Fix: Marshmallow Not Working — Schema Errors, Load vs Dump, and Field Validation
How to fix Marshmallow errors — Schema not validated on dump, ValidationError messages format, unknown field handling, missing vs default, post_load object construction, and Marshmallow 3 to 4 migration.