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. 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, …):

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

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

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.

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.

Released under the MIT License.