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
  • __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

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 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__ Hook

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)

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: 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: 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

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.0.tar.gz (24.3 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.0-py3-none-any.whl (9.6 kB view details)

Uploaded Python 3

File details

Details for the file fastcbv-0.1.0.tar.gz.

File metadata

  • Download URL: fastcbv-0.1.0.tar.gz
  • Upload date:
  • Size: 24.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","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.0.tar.gz
Algorithm Hash digest
SHA256 d3536f4658a1d6667d2929a873ae395e8b85712cdccab4c6f37af6870d71106e
MD5 78a4da37584d4a7ff780f71ee8bccde3
BLAKE2b-256 9a0317104ff673549b9a29ead0b623356a5460793006e0476f1b09360fb805e9

See more details on using hashes here.

File details

Details for the file fastcbv-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: fastcbv-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 9.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6146439466455612f6611e32002247ff1aaa85ba4e6932da7ddb232f90f015b1
MD5 d981d1808142bf485211a58c36115ff8
BLAKE2b-256 88fbb6d8229cf89535254a04bdd073093e7bd1af9be49ed6a3df0ae038f8c4cb

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