Skip to content

Migration from FastAPI

Eshwar Chandra Vidhyasagar Thedla edited this page Apr 9, 2026 · 1 revision

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.


Quick Comparison

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

Step 1: Change Imports

# 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, WebSocket

The 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}

Step 2: Convert Pydantic Models to msgspec Structs

This is the biggest change. msgspec Structs replace Pydantic BaseModel.

Basic Models

# 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

Nested Models

# 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

Lists and Dicts

# 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]

Step 3: Pydantic Validators → msgspec Hooks

This is the area with the most differences.

Field Validation

# 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")

Computed Fields

# 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}"

Custom Serialization (model_dump / model_dump_json)

# 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

Field Aliases

# 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".


Step 4: Router Migration

# 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)

Step 5: Middleware

Middleware registration is identical:

# Same in both
from FasterAPI.middleware import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

Custom Middleware

# 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.dispatch takes (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.


Step 6: Background Tasks

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"}

Step 7: Dependencies (Depends)

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.


Step 8: Exception Handlers

# 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.


Edge Cases & Gotchas

1. response_model

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)

2. JSON encoding differences

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)

3. Form data

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}

4. WebSocket

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()

Migration Checklist

  • Replace fastapi imports with FasterAPI imports
  • Replace FastAPI() with Faster()
  • Replace APIRouter with FasterRouter
  • Convert BaseModel subclasses to msgspec.Struct
  • Convert Pydantic validators to __post_init__
  • Replace model_dump() with msgspec.structs.asdict()
  • Replace model_dump_json() with msgspec.json.encode()
  • Update custom middleware to ASGI-level dispatch
  • Convert yield-based dependencies to return-based
  • Test all endpoints
  • Run benchmarks to verify speedup