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. A frontend ViewSet is declared the same way as a backend one — by listing the mixins it is composed of — and exposes the same method names (list, retrieve, create, update, …):
interface Item { id: number; name: string }
class ItemApi extends restViewSet<Item>()('id', [BulkViewSetMixin, LookupMixin]) {}
const itemsApi = new ItemApi({ basePath: '/items' });
const all = await itemsApi.list();
const one = await itemsApi.retrieve(1);
const saved = await itemsApi.create({ name: 'Widget' });
await itemsApi.listCursor(); // TS2339: this ViewSet declares no cursor listThe empty () is required: TypeScript has no partial type-argument inference, so Item cannot be given explicitly while the pk field and the mixin list are inferred in the same call.
TypeScript enforces that you only call methods the backend ViewSet actually exposes — the mixin list is the single source of truth for the contract. It is written once, as values, and does three jobs: it decides which actions the class exposes to callers, it types them, and it is handed to the proxy, which compares it against GET /items/schema on construction and warns about any disagreement.
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.
Until a Django ORM-backed implementation class ships, see Django Integration for the recommended convention (plain sync functions + sync_to_async) and everything else a Django-backed project needs (process init, autodiscover_tasks, decorator combination).
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.
