Python Mixins
The mixin system is the core of fastapi-viewsets. Each mixin adds one or more HTTP endpoints to a viewset class. You compose viewsets by inheriting from the mixins you need.
How it works
Every mixin class owns a private APIRouter and registers its endpoints on it using standard FastAPI decorators. The route_viewset decorator later collects all routes from the class hierarchy, resolves generic type parameters, and registers them on your application router.
You implement the actual business logic by overriding the corresponding perform_* method from ImplMixin. Every perform_* method takes a context: Context as its first parameter after self - see Architecture for the request pipeline that builds it, and Context Processors for how to use it.
Individual operation mixins
Create
| Class | Endpoint | Description |
|---|---|---|
CreateMixin[K, T] | POST / | Create a single record |
BulkOnlyCreateMixin[K, T] | POST /bulk | Create multiple records |
BulkCreateMixin[K, T] | both above | Single + bulk create |
from fastapi_viewsets.context import Context
from fastapi_viewsets.mixins import BulkCreateMixin
class MyViewSet(BulkCreateMixin[int, Item]):
async def perform_create(self, context: Context, data: Item) -> Item:
db.append(data)
return data
async def perform_bulk_create(self, context: Context, data: list[Item]) -> list[Item]:
db.extend(data)
return dataList & Retrieve
| Class | Endpoint | Description |
|---|---|---|
ListMixin[T, TFilter] | GET / | Return all records, with optional filter and sort |
RetrieveMixin[K, T] | GET /{pk} | Return a single record by PK |
ListMixin is generic over an optional TFilter type (default: None). The endpoint always exposes a sort query parameter (comma-separated column:direction pairs). When a filter model is also provided, its fields appear as individual query parameters.
Filtering (active when any filter field is set): filter_list(fltr, records) filters in memory. It has no default implementation, so a viewset that declares a filter type implements it — or declares the filter with make_filter_model, which needs no code at all. See Declarative filters.
Sorting (active when the sort query parameter is given): sort_list(sort, records) sorts in memory. The default is a stable multi-key sort with nulls lowest in both directions.
To do either in the backend instead, override apply_filter or apply_sort — see the list pipeline.
from fastapi_viewsets.context import Context
from fastapi_viewsets.mixins import ListMixin, RetrieveMixin, make_all_optional, SortState
from fastapi_viewsets.response_classes import NotFoundError
ItemFilter = make_all_optional(Item)
class MyViewSet(ListMixin[Item, ItemFilter], RetrieveMixin[int, Item]):
async def perform_list(self, context: Context) -> list[Item]:
return list(db.values())
async def filter_list(self, fltr: ItemFilter, records: list[Item]) -> list[Item]:
# filter_list does not receive context - see Context Processors "Known limitations"
if fltr.name is not None:
records = [r for r in records if fltr.name.lower() in r.name.lower()]
return records
async def perform_retrieve(self, context: Context, pk: int) -> Item:
if pk not in db:
raise NotFoundError(pk)
return db[pk]Sorting works out of the box without any overrides:
GET /items?sort=name:asc,score:descFor server-side sorting, override apply_sort, order the records in the backend and mark the stage applied so the in-memory sort leaves them alone:
async def apply_sort(self, context, query, records):
if query.has_sort:
records = records.order_by(*(column.column_name for column in query.sort))
query.mark_applied("sort")
return await super().apply_sort(context, query, records)Update
| Class | Endpoint | Description |
|---|---|---|
UpdateMixin[K, T] | PUT /{pk}, PATCH /{pk} | Full and partial update |
BulkOnlyUpdateMixin[K, T] | PUT /bulk, PATCH /bulk | Bulk full and partial update |
BulkUpdateMixin[K, T] | all four above | Single + bulk update |
The partial parameter is False for PUT and True for PATCH.
async def perform_update(self, context: Context, pk: int, data: Item, partial: bool = True) -> Item:
if partial:
# apply only provided fields
...
else:
db[pk] = data
return db[pk]Destroy
| Class | Endpoint | Description |
|---|---|---|
DestroyMixin[K, T] | DELETE /{pk} | Delete a single record |
BulkOnlyDestroyMixin[K, T] | DELETE /bulk | Delete multiple records |
BulkDestroyMixin[K, T] | both above | Single + bulk delete |
perform_destroy should return a dict with the deleted PK as key (and any extra info as value).
Lookup
LookupMixin[TLookupFilter] adds a GET /lookup endpoint that returns a list of LookupItem objects — useful for populating select/autocomplete widgets.
The mixin is generic over an optional TLookupFilter type (default: LookupFilter with a single q: str | None field). When the filter is active, a two-phase pipeline runs:
setup_lookup_filter(fltr)— pre-filter hook (e.g., build a DB query). No-op by default.filter_lookup(fltr, items)— post-filter hook. The default implementation filters byfltr.q(case-insensitive substring match ontitle), so basic search works out of the box.
from fastapi_viewsets.mixins import LookupMixin, LookupItem, LookupFilter
# Default behaviour: q= query param, filters by title automatically
class MyViewSet(..., LookupMixin):
async def perform_lookup(self, context: Context) -> list[LookupItem]:
return [LookupItem(group=None, pk=i.id, title=i.name, icon=None) for i in db.values()]
# Custom filter model with additional fields
class MyFilter(LookupFilter):
group: str | None = None
class MyViewSet(..., LookupMixin[MyFilter]):
async def perform_lookup(self, context: Context) -> list[LookupItem]:
return [LookupItem(group=i.group, pk=i.id, title=i.name) for i in db.values()]
async def filter_lookup(self, fltr: MyFilter, items: list[LookupItem]) -> list[LookupItem]:
if fltr.q is not None:
items = [i for i in items if fltr.q.lower() in i.title.lower()]
if fltr.group is not None:
items = [i for i in items if i.group == fltr.group]
return itemsCombined viewset mixins
For convenience, three pre-composed classes are provided:
| Class | Included actions |
|---|---|
ReadOnlyViewSetMixin[K, T] | list, retrieve |
ViewSetMixin[K, T, TFilter=None] | list, retrieve, create, update, partial_update, destroy |
BulkViewSetMixin[K, T, TFilter=None] | all of the above + bulk_create, bulk_update, bulk_partial_update, bulk_destroy |
from fastapi_viewsets.mixins import BulkViewSetMixin, make_all_optional
ItemFilter = make_all_optional(Item)
class ItemViewSet(BulkViewSetMixin[int, Item, ItemFilter]):
async def perform_list(self, context): ...
async def perform_retrieve(self, context, pk): ...
async def perform_create(self, context, data): ...
async def perform_bulk_create(self, context, data): ...
async def perform_update(self, context, pk, data, partial): ...
async def perform_bulk_update(self, context, records, partial): ...
async def perform_destroy(self, context, pk): ...
async def perform_bulk_destroy(self, context, pk): ...
async def filter_list(self, fltr, records): ... # not context-threaded, see Context ProcessorsError handling
Raise NotFoundError when a record does not exist. It produces a proper 404 response with a JSON body.
from fastapi_viewsets.response_classes import NotFoundError
raise NotFoundError(pk)
# → HTTP 404: {"detail": "Item with pk 42 not found"}