fastapi-controllers
A simple solution for organizing your FastAPI endpoints
Organize your API endpoints
fastapi-controllers offers a simple solution for organizing your API endpoints into controller classes, embracing the concept of class-based views.
Features
- class-based approach to organizing FastAPI endpoints
- decorators with exactly the arguments of their FastAPI counterparts, checked by type checkers and completed by IDEs
- class-scoped APIRouter settings that subclasses inherit
- instance-scoped definition of FastAPI dependencies
- sync, async and websocket endpoints
- controller inheritance
Installation
pip install fastapi-controllers
Minimal working example
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi_controllers import create_router, get, websocket
class ExampleController:
@get("/example")
async def get_example(self) -> dict[str, str]:
return {"message": "Hello"}
@websocket("/ws")
async def ws_example(self, ws: WebSocket) -> None:
await ws.accept()
try:
while True:
data = await ws.receive_text()
await ws.send_text(f"Received: {data}")
except WebSocketDisconnect:
pass
app = FastAPI()
app.include_router(create_router(ExampleController))
A controller is a plain class whose endpoints are methods decorated with route decorators. create_router creates a FastAPI APIRouter and populates it with the controller's routes. Include it in the application in the usual way, with app.include_router. create_router raises TypeError if the class has no routes.
Save the example as example.py and serve it with an ASGI server, for example the FastAPI CLI:
pip install "fastapi[standard]"
fastapi dev example.py
Same arguments as FastAPI
Every decorator, and create_router, takes exactly the arguments of its FastAPI counterpart. There is no new naming convention to learn:
gethas the signature ofAPIRouter.get,websockethas the signature ofAPIRouter.websocket, and so on for every route decorator;routerandcreate_routertake the arguments ofAPIRouteritself;- mypy, pyright and ty check the arguments, and IDEs complete them;
- an unknown or missing decorator argument raises
TypeErroras soon as the class is defined; help()and other runtime tools show the FastAPI arguments too;- a decorated method or class keeps its type, and a decorated method stays callable as an ordinary method.
Route decorators apply to plain methods only. A route decorator on a staticmethod, a classmethod or another wrapper object raises TypeError.
from fastapi_controllers import create_router, router
from fastapi_controllers import delete, get, head, options, patch, post, put, trace, websocket
Configure the APIRouter
The router class decorator sets the APIRouter arguments of a controller. create_router takes the same arguments; a value passed to create_router replaces the value set by router.
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from pydantic import BaseModel
from fastapi_controllers import create_router, get, post, router
security = HTTPBasic()
async def authorized_user(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
) -> None: ...
class ExampleRequest(BaseModel):
name: str
class ExampleResponse(BaseModel):
message: str
@router(prefix="/example", tags=["example"], dependencies=[Depends(authorized_user)])
class ExampleController:
@get("")
async def get_example(self) -> ExampleResponse:
return ExampleResponse(message="Hello")
@post("")
async def post_example(self, data: ExampleRequest) -> ExampleResponse:
return ExampleResponse(message=f"Hello, {data.name}!")
app = FastAPI()
app.include_router(
create_router(
ExampleController,
deprecated=True,
responses={404: {"description": "Not found"}},
)
)
Warning: An argument replaces the earlier value as a whole. Lists are not combined. For example,
create_router(ExampleController, dependencies=[...])removes theauthorized_userdependency. To add a dependency, pass it toapp.include_router, which keeps the router's own dependencies.
Instance-scoped dependencies
Define instance-scoped attributes in the controller's __init__. They give every endpoint easy access to common dependencies. FastAPI resolves the __init__ dependencies and builds a fresh controller instance for each request.
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi_controllers import create_router, get, router
class DbSession:
@property
def status(self) -> str:
return "CONNECTED"
async def get_db_session() -> DbSession:
return DbSession()
@router(prefix="/example")
class ExampleController:
def __init__(self, session: Annotated[DbSession, Depends(get_db_session)]) -> None:
self.session = session
@get("")
async def get_status(self) -> dict[str, str]:
return {"status": self.session.status}
app = FastAPI()
app.include_router(create_router(ExampleController))
Route order and inheritance
FastAPI serves a request from the first registered route that matches it. create_router registers routes in this order:
- within a class, in the order the methods are defined, so
/medefined before/{item_id}takes precedence; - a base controller's routes before its subclass's routes;
- with multiple inheritance, in reverse method resolution order, so
class C(A, B)registers the routes ofBbefore those ofA; - for decorators stacked on one method, bottom-up: the decorator nearest to
defregisters first.
A subclass inherits the routes of its base controllers. To override a route, redefine the method and decorate it again. The override keeps the original route's position. create_router raises TypeError if a subclass redefines a route method without a route decorator.
A subclass also inherits the router settings of its base controllers. The settings merge argument by argument: a subclass's router value replaces its base's, and a create_router argument replaces both.
from fastapi_controllers import get, router
@router(prefix="/base", tags=["items"])
class BaseController:
@get("/me")
async def me(self) -> str:
return "me"
@get("/{item_id}")
async def item(self, item_id: int) -> int:
return item_id
@router(prefix="/child") # keeps tags=["items"]
class ChildController(BaseController):
@get("/me")
async def me(self) -> str:
return "child"
Migrating from 0.3.x
Releases before 1.0.0 may break the API. Each break comes with a migration step in this section.
Warning: The
Controllerclass is gone, and controllers no longer read theprefix,tagsanddependenciesclass attributes. They are now ordinary class attributes, so nothing raises an error when they are set. Router dependencies defined there, such as authentication, stop running, and their routes become public. Move these attributes to@router(...)(step 3) before you deploy the upgrade.create_routerraisesTypeErroronly for a controller that still defines__router_params__.
This command finds the code that steps 2, 3 and 5 change:
grep -rnE '\bController\b|\.create_router\(|__router_params__|fastapi_controllers\.(controllers|definitions|helpers|routing)|\.route_(args|kwargs|meta)' --include='*.py' .
Steps 4 and 6 need a review of each controller.
1. Update the requirements
fastapi-controllers now needs Python 3.11 or later and FastAPI 0.124.0 or later. Version 0.3.x needed Python 3.8 or later and any FastAPI.
2. Turn controllers into plain classes
Remove the Controller base class from every controller. Replace every ExampleController.create_router() call with create_router(ExampleController), and import create_router from fastapi_controllers.
3. Move the router settings to @router
Move the prefix, tags and dependencies class attributes, and every __router_params__ entry, into the router class decorator. It takes every APIRouter argument. In 0.3.x, a __router_params__ value overrode the class attribute of the same name. If a controller sets both, keep the __router_params__ value.
In 0.3.x, subclasses shared and changed their base's __router_params__, so a subclass could silently use another controller's prefix. Check the paths of every subclass after you migrate.
Before (0.3.x):
from fastapi import Depends, FastAPI
from fastapi_controllers import Controller, get
async def authorized_user() -> None: ...
class ExampleController(Controller):
prefix = "/example"
__router_params__ = {"dependencies": [Depends(authorized_user)], "deprecated": True}
@get("")
async def get_example(self) -> str:
return "Hello"
app = FastAPI()
app.include_router(ExampleController.create_router())
After:
from fastapi import Depends, FastAPI
from fastapi_controllers import create_router, get, router
async def authorized_user() -> None: ...
@router(prefix="/example", dependencies=[Depends(authorized_user)], deprecated=True)
class ExampleController:
@get("")
async def get_example(self) -> str:
return "Hello"
app = FastAPI()
app.include_router(create_router(ExampleController))
4. Give every endpoint a self parameter
The first parameter of every decorated method now receives the controller instance, whatever its name. In 0.3.x, only a parameter named self received it, so a method without self got its first parameter from the request. Add self as the first parameter of every decorated method.
Before (0.3.x):
from fastapi import WebSocket
from fastapi_controllers import Controller, websocket
class ExampleController(Controller):
@websocket("/ws")
async def ws_example(websocket: WebSocket) -> None:
await websocket.accept()
await websocket.close()
After:
from fastapi import WebSocket
from fastapi_controllers import websocket
class ExampleController:
@websocket("/ws")
async def ws_example(self, websocket: WebSocket) -> None:
await websocket.accept()
await websocket.close()
5. Stop reading routes from the class
The decorators now return the method unchanged. ExampleController.get_example is a plain function, not a Route object, so .endpoint, .route_args, .route_kwargs and .route_meta no longer exist. To inspect the routes, read create_router(ExampleController).routes.
The modules fastapi_controllers.controllers, fastapi_controllers.definitions, fastapi_controllers.helpers and fastapi_controllers.routing no longer exist. Import everything from fastapi_controllers.
6. Check the route order
In 0.3.x, routes were registered in alphabetical order of their method names. They now follow the order in Route order and inheritance. Where one path can match another, such as /{item_id} and /me, define the more specific route first.
In 0.3.x, a subclass method that overrode a route without a route decorator silently removed the route. create_router now raises TypeError for it. Decorate the override again.
Metadata
Release files for fastapi-controllers 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastapi_controllers-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Release files / fastapi_controllers-0.4.0-py3-none-any.whl
| Download URL | fastapi_controllers-0.4.0-py3-none-any.whl |
|---|---|
| Size | 9.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
108e63f556bfd249eb70bc11b1181d0d570db5d4c99bead8b33abe81e346ec97
|
|
BLAKE2b-256 checksum How to use checksums |
15b2818c1204b19752b24976aa6738fd8ccbaeaf9395df047f1f7e6ae5c6f9df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency log