Class-based views for FastAPI
Project description
fastcbv
Class-based views for FastAPI.
Installation
uv add fastcbv
Quick Start
from 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"])
Features
- Define HTTP methods as class methods (
get,post,put,patch,delete,head,options) - Class-level dependencies with FastAPI's
DependsandAnnotated __prepare__hook for shared setup logic across methods- Full support for path parameters, query parameters, and request bodies
- Full support for
from __future__ import annotations - View inheritance for reusable patterns
- Access to the request object via
self.request
Basic Usage
Simple View
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
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)
Class-Level Dependencies
Use Annotated with Depends as class attributes to share dependencies across all methods:
from typing import Annotated
from fastapi import Depends
def get_db():
return Database()
class ItemView(BaseView):
db: Annotated[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__ Hook
The __prepare__ method runs before every HTTP method. Use it for common setup like loading resources:
class ItemView(BaseView):
db: Annotated[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)
Query Parameters
Use Annotated with Query for validated query parameters:
from typing import Annotated
from fastapi import Query
class ItemView(BaseView):
async def get(
self,
limit: Annotated[int, Query(ge=1, le=100)] = 10,
) -> list[dict]:
return await get_items(limit=limit)
Custom Status Codes
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:
pass
Patterns
Authentication with Inheritance
Create 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}
Shared Dependencies via Inheritance
class DatabaseView(BaseView):
"""Base view with database access."""
db: Annotated[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()
Views with Helper Methods
Organize complex logic using helper methods. Methods starting with _ are ignored by the router:
class OrderView(BaseView):
db: Annotated[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.order
Accessing the Request
The 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 logic
Background Tasks
Add FastAPI's BackgroundTasks as a class-level dependency to schedule work after the response is sent:
from fastapi import BackgroundTasks
class OrderView(BaseView):
background_tasks: BackgroundTasks
db: Annotated[Database, Depends(get_db)]
@status_code(201)
async def post(self, name: str) -> dict:
order = await self.db.create_order(name)
self.background_tasks.add_task(send_confirmation_email, order.email)
self.background_tasks.add_task(update_inventory, order.items)
return order
Router Options
Tags and Prefix
router = APIRouter(prefix="/api/v1")
router.add_view("/items", ItemView, tags=["items"])
Filter Methods
Register only specific HTTP methods:
router.add_view("/items", ItemView, methods=["get", "post"])
Route Dependencies
Apply dependencies to all methods in a view:
router.add_view(
"/admin/items",
AdminItemView,
dependencies=[Depends(require_admin)],
)
API Reference
BaseView
Base class for all views. Provides:
self.request- The FastAPI/StarletteRequestobject__prepare__(*args, **kwargs)- Override to add setup logic
APIRouter
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}
@router.view(path, **options) (decorator)
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}
add_view(path, view, **options)
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 |
@status_code(code)
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.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastcbv-0.1.1.tar.gz.
File metadata
- Download URL: fastcbv-0.1.1.tar.gz
- Upload date:
- Size: 28.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06fe53d337e49eaf722db22c2252ae672633e10c35ebe648a457829bcc544e9b
|
|
| MD5 |
dc8da3811166dec1002b9d64c8979a8b
|
|
| BLAKE2b-256 |
5e1af68abe86e264a3fcbbfe8078a54ea0293b9c1a70ba1f474a5dc6b5ad90fb
|
File details
Details for the file fastcbv-0.1.1-py3-none-any.whl.
File metadata
- Download URL: fastcbv-0.1.1-py3-none-any.whl
- Upload date:
- Size: 10.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.0 {"installer":{"name":"uv","version":"0.10.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75d1dfd0e3d94f20551dca384c2bcdf52c82e621b8138b45080ecb934705c39e
|
|
| MD5 |
aa1ad2eb6cf4e13120c965177339b09b
|
|
| BLAKE2b-256 |
9dd249004504c9ec82e58e51a0d4c5ce39aa7ec2e111f42dee39d826299ece7a
|