Skip to main content

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 Depends and Annotated
  • __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/Starlette Request object
  • __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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastcbv-0.1.1.tar.gz (28.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastcbv-0.1.1-py3-none-any.whl (10.3 kB view details)

Uploaded Python 3

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

Hashes for fastcbv-0.1.1.tar.gz
Algorithm Hash digest
SHA256 06fe53d337e49eaf722db22c2252ae672633e10c35ebe648a457829bcc544e9b
MD5 dc8da3811166dec1002b9d64c8979a8b
BLAKE2b-256 5e1af68abe86e264a3fcbbfe8078a54ea0293b9c1a70ba1f474a5dc6b5ad90fb

See more details on using hashes here.

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

Hashes for fastcbv-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 75d1dfd0e3d94f20551dca384c2bcdf52c82e621b8138b45080ecb934705c39e
MD5 aa1ad2eb6cf4e13120c965177339b09b
BLAKE2b-256 9dd249004504c9ec82e58e51a0d4c5ce39aa7ec2e111f42dee39d826299ece7a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page