Skip to content

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.

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

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

python
@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_viewset registers HTTP routes that forward calls to Celery via send_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:

MixinOperations added
ListMixinGET /
RetrieveMixinGET /{pk}
CreateMixinPOST /
UpdateMixinPUT /{pk}, PATCH /{pk}
DestroyMixinDELETE /{pk}
BulkCreateMixinPOST /bulk
BulkUpdateMixinPUT /bulk, PATCH /bulk
BulkDestroyMixinDELETE /bulk
LookupMixinGET /lookup
ReadOnlyViewSetMixinlist + retrieve
ViewSetMixinfull CRUD
BulkViewSetMixinfull 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, …):

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

ClassDescription
CollectionViewSetIn-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.

Released under the MIT License.