Migrating from Litestar to FastAPI
Litestar and FastAPI are two distinct methods for developing web APIs in Python, each offering unique approaches and advantages.
FastAPI has become the most popular Python API framework. It gets 4.5 million downloads daily and big companies like OpenAI, Anthropic, and Microsoft use it. FastAPI recently added a new CLI tool with simple commands like fastapi dev
and fastapi run
to make development easier.
Litestar focuses on speed above everything else. It's faster than FastAPI because it uses msgspec instead of Pydantic for handling data. The latest version (2.16.0 from May 2025) keeps improving performance while staying easy to use.
This guide will help you move from Litestar to FastAPI. We'll look at why you might want to switch and how to do it without breaking your app.
What Is FastAPI?
FastAPI changed how people build Python APIs when it came out in 2018. Created by Sebastián Ramirez, it's now the most popular choice for new Python web projects.
FastAPI's biggest strength is that it does a lot of work for you automatically. It creates API documentation, validates incoming data, and handles errors - all without requiring additional code. The framework uses Python type hints to understand your data structures and generates OpenAPI schemas automatically. The new FastAPI CLI makes it even easier to get started with commands like fastapi dev
for development.
What makes FastAPI special is its huge ecosystem. There are thousands of plugins, tools, and tutorials available. This means you can build almost anything without starting from scratch - from database integrations to authentication systems.
What Is Litestar?
Litestar is about twice as fast as FastAPI because it uses msgspec (a faster alternative to Pydantic) for serialization and has less overhead in its request/response cycle. It also enforces strict typing, which catches errors early but requires you to be more careful about how you write code. This strict approach means less runtime surprises but potentially more development time.
The trade-off is that Litestar has a smaller community and fewer ready-made solutions. You might need to build more things yourself, but your app will run faster and use less memory.
Litestar vs. FastAPI: Quick Comparison
Here's how they stack up against each other:
Feature | Litestar | FastAPI |
---|---|---|
Speed | Much faster, uses msgspec | Fast enough for most apps |
Learning curve | Harder to learn | Easier to get started |
Documentation | Good but limited | Excellent and comprehensive |
Community | Small but responsive | Large and active |
Plugins | Few but high-quality | Thousands available |
CLI tools | Basic | Modern with dev/run/deploy |
Industry use | Growing | Used by major companies |
AI help | Limited support | Great IDE and AI support |
Testing tools | Standard Python tools | Rich testing features |
Understanding the key differences
The main difference between Litestar and FastAPI is their philosophy. Litestar prioritizes speed and minimalism, while FastAPI prioritizes developer experience and feature completeness.
Litestar keeps things minimal and fast:
# Litestar - simple and fast
import msgspec
from litestar import Litestar, get
class User(msgspec.Struct):
id: int
name: str
email: str
@get("/users/{user_id:int}")
async def get_user(user_id: int) -> User:
user_data = await fetch_user_data(user_id)
return User(**user_data)
app = Litestar(route_handlers=[get_user])
FastAPI gives you more features automatically:
# FastAPI - more features built-in
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
app = FastAPI(title="User API", version="1.0.0")
@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
user_data = await fetch_user_data(user_id)
if not user_data:
raise HTTPException(status_code=404, detail="User not found")
return User(**user_data)
Notice how FastAPI automatically creates interactive API documentation at /docs
, handles error responses in a standardized format, and gives you more control over response schemas. The response_model
parameter ensures the response matches your expected structure and generates proper OpenAPI documentation.
Moving request and response handling
When you move from Litestar to FastAPI, you'll need to adjust how you handle requests and responses. The biggest change is in how validation and error handling work.
Litestar keeps things direct:
# Litestar - straightforward approach
from litestar import get
from litestar.exceptions import HTTPException
@get("/api/users/{user_id:int}")
async def get_user(user_id: int) -> dict:
if user_id <= 0:
raise HTTPException(status_code=400, detail="Invalid user ID")
user = await user_service.get_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return {"id": user.id, "name": user.name}
FastAPI does more validation and documentation automatically:
# FastAPI - automatic validation and docs
from fastapi import FastAPI, Path, HTTPException, status
from pydantic import BaseModel
class UserResponse(BaseModel):
id: int
name: str
app = FastAPI()
@app.get("/api/users/{user_id}",
response_model=UserResponse,
summary="Get user by ID")
async def get_user(
user_id: int = Path(description="User ID", gt=0)
):
user = await user_service.get_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {user_id} not found"
)
return UserResponse(id=user.id, name=user.name)
FastAPI automatically validates that user_id
is positive (the gt=0
parameter), creates API documentation with parameter descriptions, and gives structured error responses. The Path
function lets you add validation and documentation for path parameters, while response_model
ensures type safety and generates proper API schemas.
Changing dependency injection
This is probably the biggest change you'll need to make. Both frameworks let you inject dependencies, but they work differently. Understanding this difference is crucial for a successful migration.
Litestar uses explicit dependency declarations:
# Litestar - explicit dependencies
from litestar import get
from litestar.di import Provide
async def get_database():
return DatabaseService("postgresql://localhost/db")
async def get_user_service(db):
return UserService(db)
@get("/users/{user_id:int}", dependencies={
"user_service": Provide(get_user_service),
"db": Provide(get_database)
})
async def get_user(user_id: int, user_service) -> dict:
return await user_service.find_by_id(user_id)
In Litestar, you explicitly declare which dependencies each endpoint needs in the decorator. This gives you fine control but requires more setup.
FastAPI uses function signatures and type hints:
# FastAPI - automatic dependency resolution
from fastapi import FastAPI, Depends
app = FastAPI()
async def get_database():
return DatabaseService("postgresql://localhost/db")
async def get_user_service(db = Depends(get_database)):
return UserService(db)
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service = Depends(get_user_service)
):
user = await user_service.find_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
FastAPI's approach is more intuitive for most developers because it uses standard Python function signatures. The Depends()
function tells FastAPI to inject the dependency, and the framework automatically handles the dependency graph. This provides better IDE support with autocomplete and type checking, but it's a bit slower due to the extra inspection FastAPI does.
Handling middleware and startup/shutdown
Middleware works differently in each framework, and understanding the execution order is important.
Litestar middleware:
# Litestar - simple middleware
from litestar import Litestar, Middleware
import time
async def timing_middleware(request, call_next):
start_time = time.time()
response = await call_next(request)
response.headers["X-Process-Time"] = str(time.time() - start_time)
return response
app = Litestar(middleware=[Middleware(timing_middleware)])
FastAPI middleware with more built-in options:
# FastAPI - more middleware options
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
import time
app = FastAPI()
# Built-in middleware for common tasks
app.add_middleware(CORSMiddleware, allow_origins=["*"])
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
response.headers["X-Process-Time"] = str(time.time() - start_time)
return response
FastAPI comes with several built-in middleware options like CORS, HTTPS redirect, and trusted hosts. The middleware execution order is important: in FastAPI, middleware added later executes first (LIFO - Last In, First Out).
For startup and shutdown events, FastAPI has moved to a more modern approach:
# Litestar - simple events
app = Litestar(on_startup=[database.connect])
# FastAPI - modern lifespan pattern
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
await database.connect()
print("Database connected")
yield # App runs here
# Shutdown
await database.disconnect()
print("Database disconnected")
app = FastAPI(lifespan=lifespan)
The lifespan pattern is more powerful because it guarantees cleanup even if the app crashes, and it's easier to manage complex startup/shutdown sequences.
Security And authentication
Both frameworks handle security, but FastAPI has more built-in tools and better integration with OpenAPI security schemes.
Litestar requires more manual work:
# Litestar - manual auth setup
from litestar.middleware import AbstractAuthenticationMiddleware
import jwt
class JWTAuthenticationMiddleware(AbstractAuthenticationMiddleware):
async def authenticate_request(self, connection):
token = connection.headers.get("Authorization", "").replace("Bearer ", "")
if token:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return await get_user_by_id(payload["user_id"])
except jwt.JWTError:
return None
return None
FastAPI has built-in security tools:
# FastAPI - built-in security
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
import jwt
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
user = await get_user_by_id(payload["user_id"])
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return user
except jwt.JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
@app.get("/protected")
async def protected_endpoint(user = Depends(get_current_user)):
return {"message": f"Hello, {user.name}"}
FastAPI automatically generates security documentation in the OpenAPI schema, shows a "lock" icon in the docs for protected endpoints, and integrates with IDEs to show which endpoints require authentication. The OAuth2PasswordBearer
automatically adds the proper security scheme to your API documentation.
Data validation: msgspec vs Pydantic
This is where you'll see the biggest difference in philosophy and approach to data handling.
Litestar uses msgspec for speed:
# Litestar - msgspec for speed
import msgspec
from litestar import post
class CreateUserRequest(msgspec.Struct):
name: str
email: str
age: int
def __post_init__(self):
if self.age < 18:
raise ValueError("Must be 18 or older")
if "@" not in self.email:
raise ValueError("Invalid email")
@post("/users/")
async def create_user(data: CreateUserRequest):
user = await user_service.create(data)
return user
msgspec is faster because it's written in C and optimized for performance. However, validation logic goes in __post_init__
methods, which means less declarative validation.
FastAPI uses Pydantic for features:
# FastAPI - Pydantic for features
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field, validator, EmailStr
class CreateUserRequest(BaseModel):
name: str = Field(min_length=2, max_length=50, description="User's name")
email: EmailStr = Field(description="Valid email address")
age: int = Field(ge=18, le=120, description="User's age")
@validator('name')
def validate_name(cls, v):
if not v.strip():
raise ValueError('Name cannot be empty')
return v.strip().title()
@validator('email')
def validate_email_domain(cls, v):
allowed_domains = ['company.com', 'partner.org']
domain = str(v).split('@')[1]
if domain not in allowed_domains:
raise ValueError(f'Email must be from: {allowed_domains}')
return v
class Config:
schema_extra = {
"example": {
"name": "John Doe",
"email": "john@company.com",
"age": 30
}
}
app = FastAPI()
@app.post("/users/", status_code=status.HTTP_201_CREATED)
async def create_user(user_data: CreateUserRequest):
try:
user = await user_service.create(user_data.dict())
return user
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
Pydantic gives you better error messages (it tells you exactly which field failed and why), automatic documentation with examples, and more validation options. The Field
function lets you add constraints and descriptions that appear in the API documentation. The trade-off is that Pydantic is slower than msgspec, but for most applications, this difference isn't noticeable.
Error handling
FastAPI has more sophisticated error handling with better context and logging integration.
Litestar keeps it simple:
# Litestar - simple error handling
from litestar import Litestar, get, Response
from litestar.exceptions import HTTPException
class UserNotFoundError(Exception):
pass
async def handle_user_not_found(request, exc):
return Response(
content={"error": "User not found", "detail": str(exc)},
status_code=404,
media_type="application/json"
)
@get("/users/{user_id:int}")
async def get_user(user_id: int):
user = await user_service.find_by_id(user_id)
if not user:
raise UserNotFoundError(f"User {user_id} not found")
return {"id": user.id, "name": user.name}
app = Litestar(
route_handlers=[get_user],
exception_handlers={UserNotFoundError: handle_user_not_found}
)
FastAPI gives you more control and context:
# FastAPI - detailed error handling
from fastapi import FastAPI, HTTPException, Request, status
from fastapi.responses import JSONResponse
import logging
logger = logging.getLogger(__name__)
class UserNotFoundError(Exception):
def __init__(self, user_id: int):
self.user_id = user_id
super().__init__(f"User {user_id} not found")
app = FastAPI()
@app.exception_handler(UserNotFoundError)
async def user_not_found_handler(request: Request, exc: UserNotFoundError):
logger.warning(f"User not found: {exc.user_id} from {request.client.host}")
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={
"error": "User Not Found",
"message": str(exc),
"user_id": exc.user_id,
"path": request.url.path,
"timestamp": datetime.utcnow().isoformat()
}
)
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await user_service.find_by_id(user_id)
if not user:
raise UserNotFoundError(user_id)
return {"id": user.id, "name": user.name}
FastAPI's error handlers have access to the full request context, making it easier to log useful information for debugging. You can include client IP, request path, headers, and other context in your error responses and logs.
Organizing your routes
FastAPI encourages better organization through routers, which help you modularize large applications.
Litestar uses controllers:
# Litestar - controller-based
from litestar import Controller, get, post
class UserController(Controller):
path = "/users"
tags = ["users"] # For documentation grouping
@get("/")
async def list_users(self):
return await user_service.get_all()
@get("/{user_id:int}")
async def get_user(self, user_id: int):
return await user_service.get_by_id(user_id)
@post("/")
async def create_user(self, data):
return await user_service.create(data)
app = Litestar(route_handlers=[UserController])
FastAPI uses routers with dependency injection:
# FastAPI - router-based with dependencies
from fastapi import APIRouter, Depends, HTTPException, status
# Create a router for user-related endpoints
user_router = APIRouter(
prefix="/users",
tags=["users"],
responses={404: {"description": "Not found"}}
)
async def get_user_service():
return UserService()
@user_router.get("/", summary="List all users")
async def list_users(
skip: int = 0,
limit: int = 100,
user_service = Depends(get_user_service)
):
"""
Retrieve a list of users with pagination.
- **skip**: Number of users to skip (for pagination)
- **limit**: Maximum number of users to return
"""
return await user_service.get_all(skip=skip, limit=limit)
@user_router.get("/{user_id}", summary="Get user by ID")
async def get_user(
user_id: int,
user_service = Depends(get_user_service)
):
user = await user_service.get_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
# Main app
app = FastAPI()
app.include_router(user_router, prefix="/api/v1")
FastAPI routers make it easy to organize large applications into modules. You can define common dependencies, error responses, and tags at the router level. The router approach also makes it easier to version your API by including different routers with different prefixes.
Final thoughts
Moving from Litestar to FastAPI is about choosing between performance and developer productivity. FastAPI's ecosystem, tooling, and support offer long-term value despite performance trade-offs.
Migration needs careful planning and testing, but this guide's step-by-step approach helps avoid pitfalls. Start with simple endpoints, test, monitor, and migrate gradually.