Class-based views (CBV) for FastAPI routers — a small, maintained drop-in
replacement for the unmaintained fastapi_utils.cbv. Works on modern FastAPI
(tested from 0.115), and in particular survives FastAPI 0.137+, where
fastapi_utils.cbv breaks.
fastapi_utils.cbv relied on APIRouter.include_router eagerly copying
APIRoute objects into router.routes. FastAPI 0.137 made include_router
lazy, which broke fastapi_utils whenever two @cbv decorators shared one
router.
fastapi-cbv-router rebuilds each endpoint through the router's public
add_api_route / add_api_websocket_route API, which stays eager and
delegates all route-state computation back to FastAPI. Route configuration is
copied by introspecting add_api_route's own parameters, so the package adapts
to upstream FastAPI changes instead of hard-coding a kwarg list.
- One small decorator,
@cbv(router)— no base classes or metaclasses required. - Share a single dependency instance (
self) across every endpoint on a class. - Inject dependencies either as class-level annotated attributes or through a
custom
__init__— use whichever fits your style. - Works with regular HTTP routes and WebSocket routes.
- Preserves all route configuration (
status_code,response_model,dependencies, tags, …) and the routerprefix. - Fully typed (
py.typed), zero dependencies beyond FastAPI.
- Python: 3.12+
- FastAPI: 0.115+ — CI runs the test suite against Python 3.12/3.13 crossed
with FastAPI 0.115.0, 0.137.2, and the latest release. The suite specifically
covers FastAPI 0.137+, where
include_routerbecame lazy and brokefastapi_utils.cbv.
pip install fastapi-cbv-router
# or
uv add fastapi-cbv-routerfrom fastapi import APIRouter, Depends, FastAPI
from fastapi_cbv_router import cbv
router = APIRouter(prefix="/items")
def get_db() -> str:
return "db-connection"
@cbv(router)
class ItemsView:
db: str = Depends(get_db)
@router.get("/")
def list_items(self) -> dict:
return {"db": self.db, "items": []}
@router.post("/")
def create_item(self, name: str) -> dict:
return {"db": self.db, "created": name}
app = FastAPI()
app.include_router(router)Class-level annotated attributes become keyword-only dependencies injected via
Depends(ItemsView) and are available as self.<attr> in every endpoint.
ClassVar-annotated attributes are treated as plain class constants and are not
injected.
If you prefer constructor injection — for example to keep a clean abstract base
and wire dependencies explicitly — define a custom __init__. Its parameters
are resolved by FastAPI when the view is constructed, exactly like an endpoint's
parameters:
import abc
from http import HTTPStatus
from fastapi import APIRouter, Depends
from fastapi_cbv_router import cbv
router = APIRouter(prefix="/items", tags=["items"])
def get_service() -> "ItemService":
...
class ItemApi(abc.ABC):
@abc.abstractmethod
async def get_item(self, item_id: int) -> dict: ...
@cbv(router)
class HttpItemApi(ItemApi):
def __init__(self, service: "ItemService" = Depends(get_service)) -> None:
self.service = service
@router.get("/{item_id}", status_code=HTTPStatus.OK)
async def get_item(self, item_id: int) -> dict:
return await self.service.fetch(item_id)This pairs cleanly with DI containers such as
dependency-injector: use
Depends(Provide[...]) defaults on the @inject-decorated __init__.
WebSocket endpoints are supported the same way as HTTP routes:
from fastapi import APIRouter, WebSocket
from fastapi_cbv_router import cbv
router = APIRouter()
@cbv(router)
class ChatView:
@router.websocket("/ws")
async def chat(self, websocket: WebSocket) -> None:
await websocket.accept()
await websocket.send_text("hello")
await websocket.close()Only the plain @cbv(router) form is supported (no *urls / set_responses
helpers). This is intentional — it covers the common case with the smallest,
most maintainable surface.
MIT