ViewSets
A ViewSet bundles multiple related CRUD actions into a single class. Instead of writing five separate functions and wiring them to five separate routes, you write one class with five methods and register it once.
AsyncAPIViewSet
AsyncAPIViewSet is the main async ViewSet. It combines all five standard CRUD actions:
| Method | Action | HTTP | Path | Status |
|---|---|---|---|---|
list |
List all resources | GET | / |
200 |
create |
Create a new resource | POST | / |
201 |
retrieve |
Fetch a single resource | GET | /{id} |
200 |
update |
Replace a resource | PUT | /{id} |
200 |
destroy |
Delete a resource | DELETE | /{id} |
204 |
from typing import ClassVar, Optional
from uuid import UUID
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi_views import ViewRouter, configure_app
from fastapi_views.views.viewsets import AsyncAPIViewSet
class UpdateItemSchema(BaseModel):
name: str
price: int
class ItemSchema(BaseModel):
id: UUID
name: str
price: int
class ItemViewSet(AsyncAPIViewSet):
api_component_name = "Item"
response_schema = ItemSchema
items: ClassVar[dict[UUID, ItemSchema]] = {}
async def list(self) -> list[ItemSchema]:
return list(self.items.values())
async def create(self, item: ItemSchema) -> ItemSchema:
self.items[item.id] = item
return item
async def retrieve(self, id: UUID) -> Optional[ItemSchema]:
return self.items.get(id) # None → 404 Not Found automatically
async def update(self, id: UUID, item: UpdateItemSchema) -> ItemSchema:
self.items[id] = ItemSchema(id=id, **item.model_dump())
return self.items[id]
async def destroy(self, id: UUID) -> None:
self.items.pop(id, None)
router = ViewRouter(prefix="/items")
router.register_view(ItemViewSet)
app = FastAPI(title="Items API")
app.include_router(router)
configure_app(app)
api_component_name
This string is used to:
- Build human-readable route names shown in the OpenAPI UI (e.g., "List Item", "Create Item").
- Build stable OpenAPI operation IDs (e.g.,
list_item,create_item,retrieve_item).
Setting it explicitly is recommended so that generated clients get predictable method names.
response_schema
The Pydantic model used to serialize and validate every response body. The list action automatically wraps this in list[response_schema].
Partial ViewSets
You do not need to expose all five actions. Use a pre-built combination class or compose your own from individual mixins.
Pre-built combinations
| Class | Actions |
|---|---|
AsyncReadOnlyAPIViewSet |
list, retrieve |
AsyncListCreateAPIViewSet |
list, create |
AsyncRetrieveUpdateAPIViewSet |
retrieve, update |
AsyncRetrieveUpdateDestroyAPIViewSet |
retrieve, update, destroy |
AsyncListRetrieveUpdateDestroyAPIViewSet |
list, retrieve, update, destroy |
AsyncListCreateDestroyAPIViewSet |
list, create, destroy |
All have synchronous counterparts without the Async prefix.
from fastapi_views.views.viewsets import AsyncReadOnlyAPIViewSet
class ItemReadOnlyViewSet(AsyncReadOnlyAPIViewSet):
api_component_name = "Item"
response_schema = ItemSchema
async def list(self) -> list[ItemSchema]:
return list(items.values())
async def retrieve(self, id: UUID) -> Optional[ItemSchema]:
return items.get(id)
Custom combination with partial_update
AsyncAPIViewSet does not include partial_update (PATCH) by default. To add it, inherit from the individual mixins directly:
from abc import ABC
from fastapi_views.views.api import (
AsyncListAPIView,
AsyncCreateAPIView,
AsyncRetrieveAPIView,
AsyncPartialUpdateAPIView,
AsyncDestroyAPIView,
)
class MyViewSet(
AsyncListAPIView,
AsyncCreateAPIView,
AsyncRetrieveAPIView,
AsyncPartialUpdateAPIView,
AsyncDestroyAPIView,
ABC,
):
api_component_name = "Item"
response_schema = ItemSchema
async def list(self) -> list[ItemSchema]: ...
async def create(self, item: ItemSchema) -> ItemSchema: ...
async def retrieve(self, id: UUID) -> Optional[ItemSchema]: ...
async def partial_update(self, id: UUID, item: ItemSchema) -> ItemSchema: ...
async def destroy(self, id: UUID) -> None: ...
Adding custom routes
Use the @get, @post, @put, @patch, or @delete decorators inside any ViewSet to add non-standard endpoints. These work alongside the standard CRUD actions:
from uuid import uuid4
from fastapi_views.views.functools import get, post
class ItemViewSet(AsyncAPIViewSet):
api_component_name = "Item"
response_schema = ItemSchema
items: ClassVar[dict[UUID, ItemSchema]] = {}
async def list(self) -> list[ItemSchema]:
return list(self.items.values())
# ... other standard actions ...
@get("/search", response_model=list[ItemSchema])
async def search(self, name: str) -> list[ItemSchema]:
return [i for i in self.items.values() if name.lower() in i.name.lower()]
@post("/{id}/duplicate", status_code=201)
async def duplicate(self, id: UUID) -> ItemSchema:
original = self.items[id]
new_item = ItemSchema(
id=uuid4(),
name=f"Copy of {original.name}",
price=original.price,
)
self.items[new_item.id] = new_item
return new_item
Overriding default status codes
Override the default status code for any action using the @override decorator:
from fastapi_views.views.functools import override
from starlette.status import HTTP_200_OK
class ItemViewSet(AsyncAPIViewSet):
api_component_name = "Item"
response_schema = ItemSchema
@override(status_code=HTTP_200_OK)
async def create(self, item: ItemSchema) -> ItemSchema:
# Returns 200 instead of the default 201
...
Documenting error responses
Declare which errors an action may return by setting errors on the class. They are automatically included in the OpenAPI spec for all routes on that ViewSet:
from fastapi_views.exceptions import NotFound, Conflict
from fastapi_views.views.viewsets import AsyncAPIViewSet
class ItemViewSet(AsyncAPIViewSet):
api_component_name = "Item"
response_schema = ItemSchema
errors = (NotFound, Conflict)
async def retrieve(self, id: UUID) -> Optional[ItemSchema]:
return items.get(id)
Sync ViewSet
Replace every Async prefix with the synchronous variant when your handlers are not coroutines:
from fastapi_views.views.viewsets import APIViewSet
class SyncItemViewSet(APIViewSet):
api_component_name = "Item"
response_schema = ItemSchema
def list(self) -> list[ItemSchema]:
return list(items.values())
def retrieve(self, id: UUID) -> Optional[ItemSchema]:
return items.get(id)
def create(self, item: ItemSchema) -> ItemSchema:
items[item.id] = item
return item
def update(self, id: UUID, item: ItemSchema) -> ItemSchema:
items[id] = item
return item
def destroy(self, id: UUID) -> None:
items.pop(id, None)
Starlette runs synchronous endpoint functions in a thread pool, so they are safe to use alongside async middleware and dependencies.
Complete example
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi_views import ViewRouter, configure_app
from fastapi_views.views.viewsets import AsyncAPIViewSet
if TYPE_CHECKING:
from uuid import UUID
class UpdateItemSchema(BaseModel):
name: str
price: int
class ItemSchema(BaseModel):
id: UUID
name: str
price: int
class MyViewSet(AsyncAPIViewSet):
api_component_name = "Item"
response_schema = ItemSchema
items: ClassVar[dict[UUID, ItemSchema]] = {}
async def list(self) -> list[ItemSchema]:
return list(self.items.values())
async def create(self, item: ItemSchema) -> ItemSchema:
self.items[item.id] = item
return item
async def retrieve(self, id: UUID) -> ItemSchema | None:
return self.items.get(id)
async def update(self, id: UUID, item: UpdateItemSchema) -> ItemSchema:
self.items[id] = ItemSchema(id=id, name=item.name, price=item.price)
return self.items[id]
async def destroy(self, id: UUID) -> None:
self.items.pop(id, None)
router = ViewRouter(prefix="/items")
router.register_view(MyViewSet)
app = FastAPI(title="My API")
app.include_router(router)
configure_app(app)