fastware v0.1.0 /Migrating from FastAPI
On this page

Step-by-step guide to migrating FastAPI apps to fastware: import mappings, route handler patterns, DI translation, and SSE migration.

#Migrating from FastAPI

This guide walks through converting a FastAPI application to fastware, covering import changes, pattern translations, and trade-offs.

#Why migrate?

  • Performance -- fastware uses msgspec for JSON serialization (10-75x faster than Pydantic's default JSON encoding) and bundles Granian, a Rust-based ASGI server, eliminating the need for a separately configured uvicorn.
  • Batteries included -- SSE broadcasting, dependency injection, middleware (CORS, request tracing, trusted host), test client, background tasks, and structured logging are all built in. No need for sse-starlette, python-multipart, or other third-party packages.
  • Server management -- PID files, port availability checks, signal handling, and process group leadership are handled by the framework. Start, stop, and status-check your server programmatically.

#Import mapping

The table below maps 9 common FastAPI imports to their fastware equivalents. Most imports become shorter because fastware re-exports response types and the test client from the top-level package, eliminating the need for nested submodule imports:

Import mapping
FastAPIfastware
from fastapi import FastAPIfrom fastware import Router, create_app
from fastapi import Requestfrom fastware import Request
from fastapi.responses import JSONResponsefrom fastware import JSONResponse
from fastapi.responses import HTMLResponsefrom fastware import HTMLResponse
from fastapi.responses import StreamingResponsefrom fastware import StreamResponse
from fastapi.responses import FileResponsefrom fastware import FileResponse
from fastapi import Dependsfrom fastware import DependencyResolver
from starlette.middleware.cors import CORSMiddlewarefrom fastware.middleware import CORSMiddleware
from starlette.testclient import TestClientfrom fastware.testing import TestClient

#Pattern translation

#App creation

In FastAPI, you create the application object directly and register routes on it. In fastware, routing and app construction are separate concerns, which makes the router testable independently and allows multiple routers to be composed before building the final ASGI app:

FastAPI:

python
from fastapi import FastAPI

app = FastAPI(title="My App")

fastware:

python
from fastware import Router, create_app, serve

router = Router()

# Register routes on router (see below)

app = create_app(router)

# Serve it
if __name__ == "__main__":
    serve(app, foreground=True, host="127.0.0.1", port=8000)

fastware separates routing (Router) from app construction (create_app). The Router collects route definitions; create_app wraps the router with middleware, static file serving, SPA fallback, and lifespan management.

#Route handlers

The biggest API difference between FastAPI and fastware is how route handlers access request data. FastAPI inspects handler signatures and injects parameters automatically; fastware passes an explicit Request object with typed accessor methods:

FastAPI -- signature-injected parameters:

python
@app.get("/users/{user_id}")
async def get_user(user_id: int, q: str = None):
    return {"user_id": user_id, "q": q}

fastware -- explicit Request object:

python
@router.get("/users/{user_id:int}")
async def get_user(request):
    user_id = request.path_params["user_id"]  # already an int
    q = request.query("q")                     # returns None if absent
    return {"user_id": user_id, "q": q}

Key differences:

  • Path parameter types are declared in the route pattern: {user_id:int}, {name:str}, {path:path}.
  • Query parameters are accessed via request.query(name, default=..., type_=int) with optional constraints (ge, le, min_length, max_length).
  • The handler receives a single Request object. Body is accessed via request.json (parsed dict/list) or request.body (raw bytes).
  • Return a plain dict or list and it gets auto-wrapped as a JSON response. Or return an explicit JSONResponse, HTMLResponse, etc.

#Middleware

FastAPI uses add_middleware calls after app creation, which means middleware ordering depends on call order. In fastware, all 5 built-in middleware classes (CORS, RequestID, RequestTiming, TrustedHost, ViteDevProxy) are configured declaratively via AppConfig fields, eliminating the imperative step and ensuring middleware ordering is deterministic regardless of configuration order:

FastAPI:

python
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
)

fastware:

python
from fastware import Router, AppConfig, create_app

router = Router()
app = create_app(router, config=AppConfig(
    cors_origins=["*"],
    request_id=True,       # X-Request-Id header (on by default)
    request_timing=True,   # request logging with ring buffer (on by default)
    trusted_hosts=["localhost", "myapp.example.com"],  # optional
))

Built-in middleware is configured declaratively via AppConfig. Custom middleware can be passed as a list of ASGI middleware classes via AppConfig(middleware=[...]).

#Dependency injection

FastAPI uses a Depends() wrapper function that reads the handler signature. fastware uses an explicit deps dict on route decorators, making dependencies visible in the decorator call without inspecting the handler body:

FastAPI:

python
from fastapi import Depends

async def get_db():
    db = await connect()
    try:
        yield db
    finally:
        await db.close()

@app.get("/items")
async def list_items(db=Depends(get_db)):
    return await db.fetch_all("SELECT * FROM items")

fastware:

python
from fastware import Router, DependencyResolver, create_app

async def get_db(request):
    db = await connect()
    try:
        yield db
    finally:
        await db.close()

router = Router()

@router.get("/items", deps={"db": get_db})
async def list_items(request, db):
    return await db.fetch_all("SELECT * FROM items")

app = create_app(router)

Dependencies are declared as a dict mapping names to factory callables, passed via the deps keyword on route decorators or router.include_router(other, deps={...}) for router-wide dependencies. Factory functions receive the Request object and support the yield pattern for cleanup.

#Testing

The test client pattern is nearly identical between the two frameworks. Both wrap httpx with ASGI transport to send requests directly to the ASGI application in-process, so your existing test assertions (status code checks, JSON body comparisons, header inspection) mostly transfer unchanged -- only the import path changes:

FastAPI:

python
from fastapi.testclient import TestClient

with TestClient(app) as client:
    resp = client.get("/health")
    assert resp.status_code == 200

fastware:

python
from fastware.testing import TestClient

with TestClient(app) as client:
    resp = client.get("/health")
    assert resp.status_code == 200

The pattern is the same. fastware's TestClient wraps httpx with ASGITransport. An async variant is also available:

python
from fastware.testing import AsyncTestClient

async with AsyncTestClient(app) as client:
    resp = await client.get("/health")
    assert resp.status_code == 200

#SSE (Server-Sent Events)

FastAPI requires the sse-starlette third-party package and a manual generator function pattern. fastware includes a built-in Broadcaster with typed event registration, per-client async queues, automatic disconnect pruning, and configurable heartbeat intervals:

FastAPI -- requires sse-starlette third-party package:

python
from sse_starlette.sse import EventSourceResponse

@app.get("/events")
async def events():
    async def generate():
        while True:
            yield {"data": "ping"}
            await asyncio.sleep(1)
    return EventSourceResponse(generate())

fastware -- built-in Broadcaster:

python
from fastware import Router, Broadcaster, sse_route, create_app

broadcaster = Broadcaster(heartbeat_interval=30)
broadcaster.register_event("update")

router = Router()
router.add_route("GET", "/events", sse_route(broadcaster))

@router.post("/notify")
async def notify(request):
    broadcaster.broadcast("update", {"msg": "hello"})
    return {"ok": True}

app = create_app(router)

The Broadcaster manages per-client queues, prunes disconnected clients, and optionally sends heartbeat comments to keep connections alive. See the SSE Broadcasting guide for full details.

#What's different (trade-offs)

  • No automatic OpenAPI generation. FastAPI generates an OpenAPI schema from your route signatures automatically. fastware does not currently generate OpenAPI docs.
  • No signature-based parameter injection. FastAPI inspects handler signatures to inject path params, query params, headers, and body. fastware uses an explicit Request object -- you call request.path_params, request.query(), request.json, request.header().
  • No Pydantic integration at the routing level. FastAPI validates request bodies and query params through Pydantic models declared in handler signatures. fastware supports request.json_as(MyModel) for Pydantic body validation and response_model on routes for response validation, but does not auto-parse from signatures.

#What's better

  • msgspec (10-75x faster JSON) -- all JSON serialization and deserialization uses msgspec. No Pydantic overhead on the hot path.
  • Granian (managed server lifecycle) -- serve() handles PID files, port checking, signal forwarding, and process group management. No separate uvicorn.run() configuration needed.
  • Built-in SSE -- the Broadcaster provides typed events, per-client queues, auto-pruning, and heartbeats with zero third-party dependencies.
  • Built-in middleware suite -- CORS, request ID propagation, request timing with ring buffer, trusted host checking, and Vite dev proxy are all included.
  • Built-in test client -- both sync and async test clients ship with fastware. No need to install Starlette or httpx separately (httpx is a dev dependency).
  • Optional deps are truly optional -- pywebview, structlog, pydantic, and websockets are late-imported only when needed. The core framework loads with only msgspec and granian.

#API reference

See the Router class for all 6 route registration methods (get, post, put, patch, delete, ws) plus add_route and include_router for programmatic registration, and the AppConfig dataclass for all 12 declarative configuration options including middleware, static files, and SPA fallback:

#Router

Simple path-based HTTP router using {param} placeholders and type coercion.

Supports {param}, {param:str}, {param:int}, and {param:path} syntax.

#mount

python
def mount(self, prefix: str, app: Any) -> None

Mount an ASGI sub-application at a path prefix.

When a request path starts with prefix, the scope is rewritten (path stripped, root_path extended) and forwarded to app. Both http and websocket scope types are forwarded.

The prefix must start with / and must not end with /. A trailing slash is stripped automatically.

#get

python
def get(self, path: str, *, deps: dict[str, Callable] | None=None, response_model: type | None=None) -> Callable

Decorator to register a GET handler.

#post

python
def post(self, path: str, *, deps: dict[str, Callable] | None=None, response_model: type | None=None) -> Callable

Decorator to register a POST handler.

#delete

python
def delete(self, path: str, *, deps: dict[str, Callable] | None=None, response_model: type | None=None) -> Callable

Decorator to register a DELETE handler.

#put

python
def put(self, path: str, *, deps: dict[str, Callable] | None=None, response_model: type | None=None) -> Callable

Decorator to register a PUT handler.

#patch

python
def patch(self, path: str, *, deps: dict[str, Callable] | None=None, response_model: type | None=None) -> Callable

Decorator to register a PATCH handler.

#add_route

python
def add_route(self, method: str, path: str, handler: Callable, *, deps: dict[str, Callable] | None=None, response_model: type | None=None) -> None

Programmatic route registration.

#ws

python
def ws(self, path: str, *, deps: dict[str, Callable] | None=None) -> Callable

Decorator to register a WebSocket handler.

#add_ws_route

python
def add_ws_route(self, path: str, handler: Callable, *, deps: dict[str, Callable] | None=None) -> None

Register a WebSocket handler for a path pattern (supports {param}).

#include_router

python
def include_router(self, other: Router, prefix: str | None=None, deps: dict[str, Callable] | None=None) -> None

Copy all routes from other into this router.

If prefix is given (e.g. "/api/v1"), its segments are prepended to every copied route's pattern.

If deps is given (a dict mapping names to factory callables), they are merged into each copied route's deps. Router-level deps are listed first so that per-handler deps can override them.

#match

python
def match(self, method: str, path: str) -> tuple[Callable, dict[str, Any]] | None

Return (handler, path_params) or None if no route matches.

Path parameter values are coerced to their declared types (e.g., {id:int} produces an int). If coercion fails the route does not match, allowing fall-through to 404.

#_match_with_deps

python
def _match_with_deps(self, method: str, path: str) -> tuple[Callable, dict[str, Any], dict[str, Callable], type | None] | None

Return (handler, path_params, deps, response_model) or None.

Internal variant of :meth:match that also returns the merged dependency dict and response_model for the matched route. Used by create_app for DI resolution and response validation.

#_match_with_path_param

python
def _match_with_path_param(pattern: list[ParsedSegment], segments: list[str], path_idx: int) -> dict[str, Any] | None

Match a route pattern containing a :path greedy parameter.

Literal/typed segments before the :path param must match exactly. Literal/typed segments after the :path param are matched from the end of the path. Everything in between is consumed by the :path parameter (joined with "/").

#match_ws

python
def match_ws(self, path: str) -> tuple[Callable, dict[str, Any]] | None

Return (handler, path_params) for a WebSocket path, or None.

#_match_ws_with_deps

python
def _match_ws_with_deps(self, path: str) -> tuple[Callable, dict[str, Any], dict[str, Callable]] | None

Return (handler, path_params, deps) for a WebSocket path, or None.

Internal variant of :meth:match_ws that also returns deps.

#AppConfig

Configuration for :func:create_app.

All fields correspond to the keyword arguments of create_app. Pass an AppConfig instance as the config parameter, and/or supply individual keyword arguments. Keyword arguments override matching fields on the config object.