Rationale
Why this library?
@dynamicforms/fastapi-viewsets was built to solve a recurring problem in full-stack development: you define the same API contract three times — once in the backend router, once in the backend business logic, and once in the frontend HTTP client. Any change ripples through all three.
This library collapses that into a single class definition.
Core design goals
Class-based routing
Instead of registering individual route functions, you define a ViewSet class whose mixin inheritance declares exactly which HTTP operations are available. The route_viewset decorator reads the class hierarchy and registers all routes automatically.
@route_viewset(router, base_path="/items", pk_field_name="id")
class ItemViewSet(CollectionViewSet[int, Item], BulkViewSetMixin[int, Item], LookupMixin):
...No manual @router.get(...) decorators. No repeated path strings.
Async CRUD for FastAPI — without boilerplate
Mixing in ViewSetMixin or BulkViewSetMixin gives you a full set of async CRUD endpoints. For in-memory data you don't write any implementation at all — just inherit from CollectionViewSet and pass your container:
class ItemViewSet(CollectionViewSet[int, Item], BulkViewSetMixin[int, Item], LookupMixin):
async def perform_lookup(self) -> list[LookupItem]:
return [LookupItem(group=None, pk=item.id, title=item.name, icon=None)
for item in await self.perform_list()]
def __init__(self):
super().__init__(container=database, pk_field="id")The CollectionViewSet provides all perform_* implementations. You only add what is genuinely custom (e.g. perform_lookup).
Transparent Celery delegation
A single decorator moves the viewset's execution to a Celery worker — no code changes to the viewset itself:
@celery_viewset(celery_app=celery_app, task_prefix="items", redis_client=redis_sync)
class ItemViewSet(CollectionViewSet[int, Item], BulkViewSetMixin[int, Item], LookupMixin):
...- When the class lives in the FastAPI process,
route_viewsetregisters HTTP routes that forward calls to Celery viasend_task. - When the class lives in the Celery worker process, it registers itself as a task handler and executes the actual logic there.
The same class, the same code — the decorator decides where it runs.
Mix-and-match capabilities
Every HTTP operation is an independent mixin. Combine only what you need:
| Mixin | Operations added |
|---|---|
ListMixin | GET / |
RetrieveMixin | GET /{pk} |
CreateMixin | POST / |
UpdateMixin | PUT /{pk}, PATCH /{pk} |
DestroyMixin | DELETE /{pk} |
BulkCreateMixin | POST /bulk |
BulkUpdateMixin | PUT /bulk, PATCH /bulk |
BulkDestroyMixin | DELETE /bulk |
LookupMixin | GET /lookup |
ReadOnlyViewSetMixin | list + retrieve |
ViewSetMixin | full CRUD |
BulkViewSetMixin | full CRUD + bulk |
Same API on the frontend
The Vue/TypeScript side mirrors the backend exactly. route_rest creates a typed HTTP proxy that exposes the same method names (list, retrieve, create, update, …):
const itemsApi = route_rest<BulkViewSetMixin<number, Item, 'id'>>(
ItemViewSet, '/items', 'id',
);
const all = await itemsApi.list();
const one = await itemsApi.retrieve(1);
const saved = await itemsApi.create({ name: 'Widget' });TypeScript enforces that you only call methods the backend ViewSet actually exposes — the generic parameter M is the single source of truth for the contract.
Built-in implementation classes
You rarely need to write perform_* methods from scratch. The library ships with ready-made implementation classes:
| Class | Description |
|---|---|
CollectionViewSet | In-memory list, set, or dict — zero boilerplate |
More backends (e.g. Django ORM ModelViewSet) are planned. For Celery, there's no separate implementation class to inherit from — see Transparent Celery delegation above: any viewset gains Celery-backed execution via the celery_viewset decorator alone.
For the full picture of what happens between a request arriving and a response going out — context processors, command middleware, and how celery_viewset fits in — see Architecture.
