Class-based views for FastAPI.
uv add fastcbvfrom fastapi import FastAPI
from fastcbv import APIRouter, BaseView
app = FastAPI()
router = APIRouter()
@router.view("/items")
class ItemView(BaseView):
async def get(self) -> dict:
return {"message": "Hello, World!"}
async def post(self, name: str) -> dict:
return {"name": name}
app.include_router(router)Or use add_view() if you prefer to keep all of your routes together:
router.add_view("/items", ItemView, tags=["items"])- Define HTTP methods as class methods (
get,post,put,patch,delete,head,options) - Class-level dependencies with FastAPI's
Depends __prepare__hook for shared setup logic across methods- Full support for path parameters, query parameters, and request bodies
- View inheritance for reusable patterns
- Access to the request object via
self.request
from fastcbv import APIRouter, BaseView
class HealthView(BaseView):
async def get(self) -> dict:
return {"status": "ok"}
router = APIRouter()
router.add_view("/health", HealthView)Path parameters can be declared in method signatures:
class ItemView(BaseView):
async def get(self, item_id: int) -> dict:
return {"item_id": item_id}
router.add_view("/items/{item_id}", ItemView)Use FastAPI's Depends as class attributes to share dependencies across all methods:
from fastapi import Depends
def get_db():
return Database()
class ItemView(BaseView):
db: Database = Depends(get_db)
async def get(self, item_id: int) -> dict:
return await self.db.get_item(item_id)
async def delete(self, item_id: int) -> None:
await self.db.delete_item(item_id)The __prepare__ method runs before every HTTP method. Use it for common setup like loading resources:
class ItemView(BaseView):
db: Database = Depends(get_db)
async def __prepare__(self, item_id: int) -> None:
self.item = await self.db.get_item(item_id)
if not self.item:
raise HTTPException(status_code=404, detail="Item not found")
async def get(self) -> dict:
return self.item
async def put(self, name: str) -> dict:
self.item["name"] = name
return self.item
async def delete(self) -> None:
await self.db.delete(self.item["id"])
router.add_view("/items/{item_id}", ItemView)Use the @status_code decorator to set response status codes:
from fastcbv import BaseView, status_code
class ItemView(BaseView):
@status_code(201)
async def post(self, name: str) -> dict:
return {"id": 1, "name": name}
@status_code(204)
async def delete(self, item_id: int) -> None:
passCreate a base view that handles authentication, then inherit from it:
from fastapi import HTTPException
from fastcbv import BaseView
class AuthenticatedView(BaseView):
"""Base view that requires authentication."""
async def __prepare__(self) -> None:
auth = self.request.headers.get("authorization")
if not auth or not auth.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Not authenticated")
self.token = auth.replace("Bearer ", "")
self.user = await get_user_from_token(self.token)
class ProfileView(AuthenticatedView):
async def get(self) -> dict:
return {"user_id": self.user.id, "name": self.user.name}
class SettingsView(AuthenticatedView):
async def get(self) -> dict:
return {"user_id": self.user.id, "settings": self.user.settings}
async def put(self, theme: str) -> dict:
self.user.settings["theme"] = theme
return {"settings": self.user.settings}You can also extend __prepare__ while calling the parent:
class ItemView(AuthenticatedView):
async def __prepare__(self, item_id: int) -> None:
await super().__prepare__() # Run auth check first
self.item = await load_item(item_id)
async def get(self) -> dict:
return {"item": self.item, "user": self.user.id}class DatabaseView(BaseView):
"""Base view with database access."""
db: Database = Depends(get_db)
class ItemView(DatabaseView):
async def get(self) -> list:
return await self.db.get_all_items()
class UserView(DatabaseView):
async def get(self) -> list:
return await self.db.get_all_users()Organize complex logic using helper methods. Methods starting with _ are ignored by the router:
class OrderView(BaseView):
db: Database = Depends(get_db)
async def __prepare__(self, order_id: int) -> None:
self.order = await self.db.get_order(order_id)
if not self.order:
raise HTTPException(status_code=404)
def _calculate_total(self) -> float:
"""Helper method for price calculation."""
subtotal = sum(item["price"] * item["qty"] for item in self.order["items"])
tax = subtotal * 0.08
return subtotal + tax
async def _send_notification(self, message: str) -> None:
"""Async helper for notifications."""
await notify_user(self.order["user_id"], message)
async def get(self) -> dict:
return {
**self.order,
"total": self._calculate_total(),
}
@status_code(200)
async def post(self, action: str) -> dict:
if action == "complete":
self.order["status"] = "completed"
await self._send_notification("Your order is complete!")
return self.orderThe request object is available via self.request:
class ItemView(BaseView):
async def get(self) -> dict:
user_agent = self.request.headers.get("user-agent", "")
client_ip = self.request.client.host
return {"user_agent": user_agent, "ip": client_ip}Properties are useful for cleanly accessing request state:
class ItemView(BaseView):
@property
def current_user_id(self) -> str | None:
"""Extract user ID from request state (set by auth middleware)."""
return getattr(self.request.state, "user_id", None)
@property
def is_admin(self) -> bool:
"""Check admin status from request state."""
return getattr(self.request.state, "is_admin", False)
async def get(self) -> dict:
return {"user_id": self.current_user_id, "admin": self.is_admin}
async def delete(self) -> None:
if not self.is_admin:
raise HTTPException(status_code=403, detail="Admin required")
# ... delete logicrouter = APIRouter(prefix="/api/v1")
router.add_view("/items", ItemView, tags=["items"])Register only specific HTTP methods:
router.add_view("/items", ItemView, methods=["get", "post"])Apply dependencies to all methods in a view:
router.add_view(
"/admin/items",
AdminItemView,
dependencies=[Depends(require_admin)],
)Base class for all views. Provides:
self.request- The FastAPI/StarletteRequestobject__prepare__(*args, **kwargs)- Override to add setup logic
Extended FastAPI router with add_view() method.
The APIRouter from fastcbv is a drop-in replacement for FastAPI's APIRouter. All existing routes, decorators, and configurations work unchanged—just swap the import and start using add_view() alongside your existing routes:
# Before
from fastapi import APIRouter
# After
from fastcbv import APIRouter
router = APIRouter(prefix="/api")
# Existing function-based routes still work
@router.get("/health")
async def health():
return {"status": "ok"}
# Add class-based views alongside them
@router.view("/items/{item_id}")
class ItemView(BaseView):
async def get(self, item_id: int) -> dict:
return {"item_id": item_id}Decorator to register a class-based view. Accepts all the same options as add_view():
@router.view("/items/{item_id}", tags=["items"], dependencies=[Depends(auth)])
class ItemView(BaseView):
async def get(self, item_id: int) -> dict:
return {"item_id": item_id}The add_view method accepts most parameters from FastAPI's add_api_route, making it familiar and compatible. Parameters that are method-specific (like status_code) or handled automatically (like endpoint and response_model) are excluded.
| Parameter | Type | Description |
|---|---|---|
path |
str |
URL path for the view |
view |
type[BaseView] |
View class to register |
methods |
list[str] |
HTTP methods to register (default: auto-detect) |
name_prefix |
str |
Prefix for route names (default: class name) |
tags |
list[str | Enum] |
OpenAPI tags |
dependencies |
Sequence[Depends] |
Dependencies for all methods |
responses |
dict[int | str, dict] |
Additional OpenAPI responses |
deprecated |
bool |
Mark all methods as deprecated |
include_in_schema |
bool |
Include in OpenAPI schema |
callbacks |
list[BaseRoute] |
OpenAPI callbacks |
openapi_extra |
dict |
Additional OpenAPI metadata |
Decorator to set the HTTP status code for a method.
Most route parameters can be configured at the router level (tags, dependencies, responses, deprecated) or derived from the method signature (parameters, return type). However, status_code is unique—it's method-specific and can't be inferred or set globally. The @status_code decorator provides a clean way to set this per-method:
from fastcbv import BaseView, status_code
class ItemView(BaseView):
async def get(self, item_id: int) -> dict:
"""Default 200 status code."""
return {"id": item_id}
@status_code(201)
async def post(self, name: str) -> dict:
"""201 Created for new resources."""
return {"id": 1, "name": name}
@status_code(204)
async def delete(self, item_id: int) -> None:
"""204 No Content for deletions."""
pass
@status_code(202)
async def put(self, item_id: int) -> dict:
"""202 Accepted for async processing."""
return {"id": item_id, "status": "processing"}Without the decorator, methods return 200 OK by default.
MIT