-
Notifications
You must be signed in to change notification settings - Fork 0
Migration from FastAPI
This guide covers everything you need to port a FastAPI application to FasterAPI. Most code stays identical — the main changes are imports, model definitions, and a few edge cases.
| FastAPI | FasterAPI | |
|---|---|---|
| Package | pip install fastapi |
pip install faster-api-web[all] |
| App class | FastAPI() |
Faster() |
| Models | pydantic.BaseModel |
msgspec.Struct |
| Router | APIRouter |
FasterRouter |
| Serialization |
json.dumps (via Pydantic) |
msgspec.json.encode (Rust) |
| Event loop | stdlib asyncio | uvloop (auto-installed) |
| Routing | Regex (Starlette) | Radix tree |
| Everything else | Same | Same |
# Before (FastAPI)
from fastapi import FastAPI, APIRouter, Depends, HTTPException, Request
from fastapi import Query, Path, Body, Header, Cookie, File, Form
from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi import BackgroundTasks, UploadFile, WebSocket
# After (FasterAPI)
from FasterAPI import Faster, FasterRouter, Depends, HTTPException, Request
from FasterAPI import Query, Path, Body, Header, Cookie, File, Form
from FasterAPI import JSONResponse, HTMLResponse, RedirectResponse
from FasterAPI.middleware import CORSMiddleware
from FasterAPI import BackgroundTasks, UploadFile, WebSocketThe decorator API is identical:
# Same in both
app = Faster() # was FastAPI()
@app.get("/users/{id}", tags=["users"], summary="Get user")
async def get_user(id: str):
return {"id": id}This is the biggest change. msgspec Structs replace Pydantic BaseModel.
# Before (Pydantic)
from pydantic import BaseModel
from typing import Optional
class User(BaseModel):
name: str
email: str
age: Optional[int] = None
# After (msgspec)
import msgspec
from typing import Optional
class User(msgspec.Struct):
name: str
email: str
age: Optional[int] = None# Before
class Address(BaseModel):
street: str
city: str
class User(BaseModel):
name: str
address: Address
# After — identical structure, different base class
class Address(msgspec.Struct):
street: str
city: str
class User(msgspec.Struct):
name: str
address: Address# Before
class Team(BaseModel):
name: str
members: list[str]
metadata: dict[str, int]
# After — same type annotations work
class Team(msgspec.Struct):
name: str
members: list[str]
metadata: dict[str, int]This is the area with the most differences.
# Before (Pydantic v2)
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
age: int
@field_validator("age")
@classmethod
def age_must_be_positive(cls, v):
if v < 0:
raise ValueError("age must be positive")
return v
# After (msgspec) — use __post_init__
class User(msgspec.Struct):
name: str
age: int
def __post_init__(self):
if self.age < 0:
raise ValueError("age must be positive")# Before (Pydantic)
class User(BaseModel):
first_name: str
last_name: str
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
# After (msgspec) — properties work the same way,
# but won't appear in JSON output by default.
# For serialized computed fields, add them as regular fields
# and set them in __post_init__:
class User(msgspec.Struct):
first_name: str
last_name: str
full_name: str = ""
def __post_init__(self):
if not self.full_name:
self.full_name = f"{self.first_name} {self.last_name}"# Before (Pydantic)
user = User(name="Alice", email="a@b.com")
data = user.model_dump() # → dict
json_bytes = user.model_dump_json() # → str
# After (msgspec)
import msgspec
user = User(name="Alice", email="a@b.com")
data = msgspec.structs.asdict(user) # → dict
json_bytes = msgspec.json.encode(user) # → bytes (not str!)
json_str = json_bytes.decode() # → str if you need it# Before (Pydantic)
class Item(BaseModel):
item_name: str = Field(alias="itemName")
# After (msgspec)
class Item(msgspec.Struct, rename="camel"):
item_name: str
# Automatically serializes/deserializes as "itemName"msgspec supports these rename strategies: "camel", "pascal", "kebab", "lower", "upper".
# Before
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/")
async def list_users():
return []
app.include_router(router)
# After — same API, different class name
from FasterAPI import FasterRouter
router = FasterRouter(prefix="/users", tags=["users"])
@router.get("/")
async def list_users():
return []
app.include_router(router)Middleware registration is identical:
# Same in both
from FasterAPI.middleware import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)# Before (Starlette)
from starlette.middleware.base import BaseHTTPMiddleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
start = time.time()
response = await call_next(request)
response.headers["X-Process-Time"] = str(time.time() - start)
return response
# After (FasterAPI) — use ASGI-level middleware for best performance
from FasterAPI.middleware import BaseHTTPMiddleware
import time
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, scope, receive, send):
start = time.perf_counter()
async def send_with_timing(message):
if message["type"] == "http.response.start":
elapsed = str(time.perf_counter() - start)
headers = list(message.get("headers", []))
headers.append((b"x-process-time", elapsed.encode()))
message = {**message, "headers": headers}
await send(message)
await self.app(scope, receive, send_with_timing)Note: FasterAPI's
BaseHTTPMiddleware.dispatchtakes(scope, receive, send)instead of(request, call_next). This is a lower-level ASGI interface — it avoids creating intermediate Request/Response objects for better performance.
No changes needed:
from FasterAPI import BackgroundTasks
@app.post("/send-email")
async def send_email(bg: BackgroundTasks):
bg.add_task(send_email_task, "user@example.com")
return {"status": "queued"}No changes needed:
from FasterAPI import Depends
async def get_db():
db = Database()
try:
yield db # Note: generator deps aren't yet supported — use return
finally:
await db.close()
# For now, use simple return-based dependencies:
async def get_db():
return Database()
@app.get("/items")
async def list_items(db = Depends(get_db)):
return await db.fetch_all("SELECT * FROM items")Current limitation: Generator/contextmanager dependencies (
yield-based) are not yet supported. Use return-based dependencies instead. This is planned for v0.2.0.
# Same in both
from FasterAPI import HTTPException
@app.get("/items/{id}")
async def get_item(id: str):
if id not in items_db:
raise HTTPException(status_code=404, detail="Item not found")
return items_db[id]
# Custom exception handler
@app.add_exception_handler(ValueError, handler)
async def value_error_handler(request, exc):
return (400, b'{"detail": "Bad value"}', [(b"content-type", b"application/json")])Note: Custom exception handlers return
(status_code, body_bytes, headers)tuples instead of Response objects. This avoids Response object allocation on the error path.
FasterAPI supports response_model in route decorators for OpenAPI schema generation, but does not filter the response through the model. The handler's return value is serialized directly.
# In FastAPI, this filters out fields not in UserOut:
@app.get("/users/{id}", response_model=UserOut)
async def get_user(id: str):
return full_user_object # Extra fields stripped
# In FasterAPI, return exactly what you want serialized:
@app.get("/users/{id}", response_model=UserOut)
async def get_user(id: str):
user = await fetch_user(id)
return UserOut(name=user.name, email=user.email)msgspec encodes bytes directly. Some types that Pydantic auto-converts will need explicit handling:
# Pydantic auto-converts datetime → ISO string
# msgspec does too, but if you return a raw dict:
from datetime import datetime
return {"created": datetime.now()} # Works in both
# For custom types, use msgspec's enc_hook:
encoder = msgspec.json.Encoder(enc_hook=custom_hook)Form handling works the same way, but uses python-multipart directly instead of going through Starlette:
@app.post("/login")
async def login(username: str = Form(), password: str = Form()):
return {"username": username}Same decorator API:
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
data = await ws.receive_text()
await ws.send_text(f"Echo: {data}")
await ws.close()- Replace
fastapiimports withFasterAPIimports - Replace
FastAPI()withFaster() - Replace
APIRouterwithFasterRouter - Convert
BaseModelsubclasses tomsgspec.Struct - Convert Pydantic validators to
__post_init__ - Replace
model_dump()withmsgspec.structs.asdict() - Replace
model_dump_json()withmsgspec.json.encode() - Update custom middleware to ASGI-level dispatch
- Convert yield-based dependencies to return-based
- Test all endpoints
- Run benchmarks to verify speedup