Coming from Starlette / FastAPI
Status: Draft; implementation checked against the development source on 2026-09-08.
If you already know Starlette or FastAPI, genro-asgi will feel familiar in the small — you still decorate a callable to make a route, params still bind to the signature, and you still get OpenAPI and Swagger for free. It differs in the large: in what you build and how features appear. This page maps the two so you can carry your intuition across and know where it stops.
The mental model, side by side
FastAPI gives you an application object; you attach path operations to it with function decorators, and a dependency-injection container supplies each operation with what it declares. Middleware and security are attached to the app; you run it under uvicorn.
genro-asgi gives you a server that mounts applications. An application is a
class whose @route-decorated methods are its endpoints. The server itself is
a composition of capability mixins — auth, sessions, tasks, plugins — that
always exist and are fed by config data rather than switched on structurally.
There is no dependency-injection container: a handler reaches what it needs
through the object graph (its application, the server, the request), not through
declared dependencies. You start the server by calling .serve() from your own
entry point, or with the genro-asgi command
(see the CLI guide).
The other pivotal difference is that routing is protocol-neutral. In FastAPI a
path operation is an HTTP thing. In genro-asgi a @route describes an operation
independently of transport, which is exactly why the same method tree can be
served as REST, as an OpenAPI schema, and as MCP tools without being rewritten.
Concept mapping
Task |
FastAPI / Starlette |
genro-asgi |
|---|---|---|
Create the app |
|
subclass |
Define a route |
|
|
Path / query params |
function args + |
method args bind to the query string, typed, with defaults |
Request body / validation |
pydantic model as a param |
the |
JSON response |
|
|
HTML response |
|
|
Dependency injection |
|
no DI container — reach through the object graph ( |
Middleware |
|
|
Auth / security |
|
|
Sessions |
|
|
Mount a sub-app |
|
|
OpenAPI / Swagger |
automatic at |
|
Start the server |
|
|
Start it from a shell |
|
|
WebSocket / streaming |
|
WSX messages or |
Tools for an AI agent |
(not built in) |
|
The same endpoint, side by side
A tiny search endpoint with a typed query parameter, returning JSON, documented in OpenAPI.
FastAPI — shown for comparison only, illustrative:
# For comparison — this is FastAPI, not genro-asgi.
from fastapi import FastAPI
app = FastAPI(title="Shop API", version="1.0.0")
@app.get("/search")
def search(q: str = "", max_price: float = 100.0) -> dict:
return {"query": q, "hits": []}
# uvicorn module:app --port 8000
genro-asgi — real, verified API:
from genro_asgi import AsgiServer, OpenApiApplication
from genro_routes import route
class Shop(OpenApiApplication):
mount = ""
openapi_info = {"title": "Shop API", "version": "1.0.0"}
@route()
def search(self, q: str = "", max_price: float = 100.0) -> dict:
return {"query": q, "hits": []}
server = AsgiServer(applications=[Shop()])
server.serve(host="127.0.0.1", port=8000)
Both answer GET /search?q=moka&max_price=30. The FastAPI version documents at
/docs and /openapi.json; the genro-asgi version at /_meta/docs and
/_meta/schema_json. The visible difference is small — a method on a class
instead of a free function, and a server that mounts the app instead of being
the app. The invisible difference is the one that matters: switch the base class
to McpOpenApiApplication and mark search with
@route(channel_channels="mcp,rest"), and the same method is now both a REST
endpoint and an MCP tool.
Honest notes on the differences
No dependency-injection container. There is no
Depends. Handlers reach collaborators through the object graph — the application holdsself.server, the request holdsself.application. If you lean heavily on FastAPI’s DI for wiring, that pattern does not port; you compose objects instead.The CLI serves a config, not an app object.
fastapi run main.pyanduvicorn main:appboth point at an ASGI callable in a module.genro-asgi serve ./config.pypoints at a configuration recipe — the file declaring the whole site — and the server builds itself from it;genro-asgi serve application=./hello.py:Hellois the closer analogue, for when there is one application and no config. The registry (--name, thenapps/stop/remove) has no FastAPI counterpart. There is still no.run()method: programmatically you build the server and call.serve(), which boots a uvicorn loop and blocks (port=0asks the OS for a free port, useful in tests). See the CLI guide.Routing is protocol-neutral by design. A
@routeis not inherently an HTTP operation. That is the reason REST, OpenAPI and MCP share one route tree — and the reason a method only becomes an MCP tool when you opt it in withchannel_channels. The flip side: think of a route as “an operation”, not “an HTTP verb on a path”.Features are config, not construction. You do not add a capability by restructuring code; auth, sessions, tasks and plugins already exist on
AsgiServerand are fed by kwargs (auth=...,middleware=...,tasks=...,plugins=...). Sessions, auth middleware and tasks are enabled by the shipped composition; explicit options can disable them.The OpenAPI prefix is
_meta. Not/docsand/openapi.json— the Swagger UI is at/_meta/docsand the schema at/_meta/schema_json.An internal
_serverapp is always mounted. Login, task management and the system OpenAPI live under/_server/...with no setup on your part — there is no FastAPI equivalent you need to wire up.
Where to go next
Getting started — the runnable hello-world.
Core concepts — the server/application model and the demux rule in full.
How-to guides — auth, sessions, OpenAPI, MCP, tasks, streaming, middleware.