If you're building REST APIs with FastAPI, you've probably written this code dozens of times: a router, a list endpoint, a get-by-id endpoint, a create, an update, a delete… and then you do it all over again for the next resource. And the next. And the next.
There's a better way. Meet fastapi-viewsets — a library inspired by Django REST Framework's ViewSets that lets you generate all six standard CRUD endpoints from a single class with one method call.
The Problem: FastAPI CRUD Is Verbose by Design
FastAPI is fantastic for building APIs. It's fast, it's type-safe, it generates OpenAPI docs automatically. But it's deliberately low-level — it gives you routing primitives, not conventions. That means every resource you build looks roughly like this:
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
router = APIRouter()
@router.get("/items", response_model=List[ItemSchema])
def list_items(limit: int = 10, offset: int = 0, db: Session = Depends(get_db)):
return db.query(Item).offset(offset).limit(limit).all()
@router.get("/items/{id}", response_model=ItemSchema)
def get_item(id: int, db: Session = Depends(get_db)):
obj = db.query(Item).filter(Item.id == id).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
return obj
@router.post("/items", response_model=ItemSchema)
def create_item(item: ItemSchema, db: Session = Depends(get_db)):
obj = Item(**item.dict())
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@router.patch("/items/{id}", response_model=ItemSchema)
def update_item(id: int, item: ItemSchema, db: Session = Depends(get_db)):
obj = db.query(Item).filter(Item.id == id).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
for key, value in item.dict(exclude_unset=True).items():
setattr(obj, key, value)
db.commit()
db.refresh(obj)
return obj
@router.delete("/items/{id}")
def delete_item(id: int, db: Session = Depends(get_db)):
obj = db.query(Item).filter(Item.id == id).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
db.delete(obj)
db.commit()
return {"status": "ok"}
That's ~50 lines of nearly identical logic. And you'll write it again for users, orders, products, categories... You get the idea.
The Solution: fastapi-viewsets
fastapi-viewsets brings DRF-style ergonomics to FastAPI. Instead of repeating yourself, you declare one class, call .register(), and all the endpoints are wired up automatically — complete with Pydantic validation, OpenAPI docs, and proper HTTP semantics.
Here's the same resource from above, rewritten with the library:
from fastapi import FastAPI
from pydantic import BaseModel, ConfigDict
from sqlalchemy import Column, Integer, String
from fastapi_viewsets import BaseViewset
from fastapi_viewsets.db_conf import Base, engine, get_session
app = FastAPI()
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True)
name = Column(String(255), nullable=False)
class ItemSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int | None = None
name: str
Base.metadata.create_all(bind=engine)
items = BaseViewset(
endpoint="/items",
model=Item,
response_model=ItemSchema,
db_session=get_session,
tags=["items"]
)
items.register(methods=["LIST", "GET", "POST", "PATCH", "DELETE"])
app.include_router(items)
That's it. ~20 lines instead of ~50, and you get the same 5 endpoints. The more resources you have, the more time you save.
What Gets Generated
A single .register() call produces these routes (using LIST, GET, POST, PATCH, DELETE):
| Method | Path | Description |
|---|---|---|
GET |
/items |
List all items (paginated) |
GET |
/items/{id} |
Get single item by ID |
POST |
/items |
Create a new item |
PATCH |
/items/{id} |
Partial update (only provided fields) |
PUT |
/items/{id} |
Full replacement |
DELETE |
/items/{id} |
Delete by ID |
Built-in pagination is included on the LIST endpoint via limit and offset query parameters — no extra code needed:
GET /items?limit=20&offset=40
Multi-ORM Support
fastapi-viewsets isn't locked to one ORM. As of v1.1.0, it ships with pluggable adapters for:
- SQLAlchemy (sync and async)
- Tortoise ORM
- Peewee
You can select your ORM via the ORM_TYPE environment variable, or pass an adapter instance directly:
pip install "fastapi-viewsets[tortoise]"
The same BaseViewset / AsyncBaseViewset interface works regardless of which adapter you're using.
Async Support Out of the Box
If your FastAPI app uses async/await (and if you care about performance, it probably should), use AsyncBaseViewset — it has the exact same interface but all handlers are async:
from fastapi_viewsets import AsyncBaseViewset
items = AsyncBaseViewset(
endpoint="/items",
model=Item,
response_model=ItemSchema,
db_session=get_async_session, # your async session factory
tags=["items"]
)
items.register(methods=["LIST", "GET", "POST", "PATCH", "DELETE"])
app.include_router(items)
Swap BaseViewset → AsyncBaseViewset and get_session → get_async_session. Nothing else changes.
Authentication in One Line
Want to protect write operations behind OAuth2? Pass your OAuth2PasswordBearer and a list of methods that require a token:
from fastapi.security import OAuth2PasswordBearer
oauth2 = OAuth2PasswordBearer(tokenUrl="/token")
router = BaseViewset(
endpoint="/items",
model=Item,
response_model=ItemSchema,
db_session=get_session,
tags=["items"]
)
router.register(
methods=["LIST", "GET", "POST", "PATCH", "DELETE"],
oauth_protect=oauth2,
protected_methods=["POST", "PATCH", "DELETE"] # read is public, writes require auth
)
GET and LIST remain open. POST, PATCH, and DELETE require a bearer token. The Swagger UI will automatically show the lock icon on protected routes.
Custom Logic: Just Subclass
BaseViewset is an APIRouter under the hood, so you can extend it just like any Python class. Override any handler method to add custom business logic:
class ItemsWithStats(BaseViewset):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add custom routes BEFORE register() to avoid path conflicts
self.add_api_route(
f"{self.endpoint}/stats",
self.collection_stats,
methods=["GET"],
tags=self.tags or [],
)
def collection_stats(self) -> dict:
return {"resource": self.endpoint.strip("/")}
def list(self, limit=10, offset=0, search=None, token=None):
# Add custom filtering logic here
return super().list(limit=limit, offset=offset)
You're not locked into the generated behavior — it's a starting point, not a cage.
How It Compares
| fastapi-viewsets | fastapi-crudrouter | Hand-rolled FastAPI | |
|---|---|---|---|
| Pattern | DRF-style ViewSet class + .register() | CRUD generator, router per resource | Full manual control |
| Boilerplate | Minimal — one class + .register() | Low — one router call per resource | Maximum |
| ORM support | SQLAlchemy, Tortoise, Peewee via adapters | SQLAlchemy, Tortoise, Ormar, Databases, Gino, In-Memory | Any ORM you integrate |
| Async | AsyncBaseViewset | Supported | Fully custom |
| Auth | OAuth2 per-method via register() | Custom dependencies | Fully custom |
| Extensibility | Subclass + override any handler | Limited override options | Unlimited |
| Pagination | limit/offset built-in | Supported | Fully custom |
| Fits existing routers | Yes — BaseViewset is an APIRouter subclass | Partial — requires specific integration | Yes |
The key difference from fastapi-crudrouter is the DRF-style ViewSet pattern — the BaseViewset is a full APIRouter subclass, so it fits naturally into any existing FastAPI app without any special integration.
Installation
pip install fastapi-viewsets
# Optional ORM extras
pip install "fastapi-viewsets[sqlalchemy]"
pip install "fastapi-viewsets[tortoise]"
pip install "fastapi-viewsets[peewee]"
What's Next
The roadmap includes:
-
Server-side search — wire the
searchquery param to real database queries - Declarative ordering and advanced filters
-
Dedicated
AsyncModelViewSetergonomics on top of SQLAlchemy 2.x async sessions - Richer OpenAPI for nested Pydantic models
Try It
The library is open source under the MIT license. If it saves you time, a ⭐ on GitHub goes a long way!
- 📦 PyPI: https://pypi.org/project/fastapi-viewsets/
- 🐙 GitHub: https://github.com/svalench/fastapi_viewsets
Stop writing the same CRUD code over and over. Your future self will thank you.
Built by Alexander Valenchits — Tech Lead @ AluSoft, Minsk.
Tags: fastapi python webdev tutorial
Top comments (0)