Class-Based Views¶
Django-Bolt supports class-based views for organizing related endpoints. This guide covers APIView, ViewSet, and ModelViewSet.
APIView¶
Use APIView to group HTTP methods for a single resource:
from django_bolt import BoltAPI
from django_bolt.views import APIView
api = BoltAPI()
@api.view("/hello")
class HelloView(APIView):
async def get(self, request):
return {"message": "Hello"}
async def post(self, request, name: str):
return {"message": f"Hello, {name}"}
This creates:
GET /hello- Calls thegetmethodPOST /hello- Calls thepostmethod
Available methods¶
Implement any of these methods:
@api.view("/resource")
class ResourceView(APIView):
async def get(self, request):
"""Handle GET requests"""
return {"method": "GET"}
async def post(self, request):
"""Handle POST requests"""
return {"method": "POST"}
async def put(self, request):
"""Handle PUT requests"""
return {"method": "PUT"}
async def patch(self, request):
"""Handle PATCH requests"""
return {"method": "PATCH"}
async def delete(self, request):
"""Handle DELETE requests"""
return {"method": "DELETE"}
Class-level configuration¶
Set authentication and permissions at the class level:
from django_bolt.auth import JWTAuthentication, IsAuthenticated
@api.view("/protected")
class ProtectedView(APIView):
auth = [JWTAuthentication()]
guards = [IsAuthenticated()]
async def get(self, request):
return {"user_id": request.user.id}
Path parameters¶
Handle path parameters in your methods:
@api.view("/users/{user_id}")
class UserView(APIView):
async def get(self, request, user_id: int):
return {"user_id": user_id}
async def put(self, request, user_id: int, data: UserUpdate):
return {"user_id": user_id, "updated": True}
ViewSet¶
ViewSet provides a higher-level abstraction for CRUD operations:
from django_bolt.views import ViewSet
@api.viewset("/items")
class ItemViewSet(ViewSet):
async def list(self, request):
"""GET /items"""
return [{"id": 1}, {"id": 2}]
async def retrieve(self, request, pk: int):
"""GET /items/{pk}"""
return {"id": pk}
async def create(self, request, item: ItemCreate):
"""POST /items"""
return {"id": 1, "created": True}
async def update(self, request, pk: int, item: ItemUpdate):
"""PUT /items/{pk}"""
return {"id": pk, "updated": True}
async def partial_update(self, request, pk: int, item: ItemPatch):
"""PATCH /items/{pk}"""
return {"id": pk, "patched": True}
async def destroy(self, request, pk: int):
"""DELETE /items/{pk}"""
return {"id": pk, "deleted": True}
This creates:
| Method | URL | Action |
|---|---|---|
| GET | /items |
list |
| POST | /items |
create |
| GET | /items/{pk} |
retrieve |
| PUT | /items/{pk} |
update |
| PATCH | /items/{pk} |
partial_update |
| DELETE | /items/{pk} |
destroy |
ModelViewSet¶
ModelViewSet provides built-in Django ORM integration:
import msgspec
from django_bolt.views import ModelViewSet
from myapp.models import Article
class ArticleSchema(msgspec.Struct):
id: int
title: str
content: str
@api.viewset("/articles")
class ArticleViewSet(ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSchema
async def list(self, request):
articles = []
async for article in await self.get_queryset():
articles.append(ArticleSchema(
id=article.id,
title=article.title,
content=article.content
))
return articles
async def retrieve(self, request, pk: int):
article = await self.get_object(pk)
return ArticleSchema(
id=article.id,
title=article.title,
content=article.content
)
get_queryset¶
Override to customize the queryset:
@api.viewset("/my-articles")
class MyArticleViewSet(ModelViewSet):
queryset = Article.objects.all()
async def get_queryset(self):
qs = await super().get_queryset()
return qs.filter(author_id=self.request.user.id)
get_object¶
Get a single object by primary key:
async def retrieve(self, request, pk: int):
article = await self.get_object(pk) # Raises 404 if not found
return ArticleSchema.from_model(article)
Custom lookup field¶
Use a different field for lookups:
@api.viewset("/articles")
class ArticleViewSet(ModelViewSet):
queryset = Article.objects.all()
lookup_field = "slug" # Use slug instead of pk
async def retrieve(self, request, slug: str):
article = await self.get_object(slug)
return {"slug": article.slug}
Custom actions¶
Add custom actions using the @action decorator to create endpoints beyond standard CRUD operations.
Basic actions¶
from django_bolt import action
@api.viewset("/articles")
class ArticleViewSet(ViewSet):
queryset = Article.objects.all()
@action(methods=["GET"], detail=False)
async def published(self, request):
"""Collection action: GET /articles/published"""
articles = []
async for article in Article.objects.filter(is_published=True):
articles.append({"id": article.id, "title": article.title})
return articles
@action(methods=["POST"], detail=True)
async def publish(self, request, pk: int):
"""Instance action: POST /articles/{pk}/publish"""
article = await self.get_object(pk)
article.is_published = True
await article.asave()
return {"published": True, "article_id": pk}
Action parameters¶
| Parameter | Description |
|---|---|
methods |
List of HTTP methods: ["GET"], ["POST"], etc. |
detail |
True for instance actions (/{pk}/action), False for collection actions (/action) |
path |
Custom URL path (defaults to function name) |
Custom path¶
Override the URL path:
@action(methods=["POST"], detail=True, path="custom-action-name")
async def some_method_name(self, request, pk: int):
"""POST /articles/{pk}/custom-action-name"""
return {"action": "custom-action-name", "article_id": pk}
Actions with query parameters¶
@action(methods=["GET"], detail=False)
async def search(self, request, query: str, limit: int = 10):
"""GET /articles/search?query=xxx&limit=5"""
articles = []
async for article in Article.objects.filter(title__icontains=query)[:limit]:
articles.append({"id": article.id, "title": article.title})
return {"query": query, "limit": limit, "results": articles}
Actions with request body¶
import msgspec
class StatusUpdate(msgspec.Struct):
is_published: bool
@action(methods=["POST"], detail=True, path="status")
async def update_status(self, request, pk: int, data: StatusUpdate):
"""POST /articles/{pk}/status with JSON body"""
article = await self.get_object(pk)
article.is_published = data.is_published
await article.asave()
return {"updated": True, "is_published": article.is_published}
Multiple methods on same path¶
Create separate actions for different HTTP methods on the same path:
@action(methods=["GET"], detail=True, path="status")
async def get_status(self, request, pk: int):
"""GET /articles/{pk}/status"""
article = await self.get_object(pk)
return {"is_published": article.is_published}
@action(methods=["POST"], detail=True, path="status")
async def update_status(self, request, pk: int, data: StatusUpdate):
"""POST /articles/{pk}/status"""
article = await self.get_object(pk)
article.is_published = data.is_published
await article.asave()
return {"updated": True}
Custom lookup field with actions¶
Actions respect the ViewSet's lookup_field:
@api.viewset("/articles")
class ArticleViewSet(ViewSet):
queryset = Article.objects.all()
lookup_field = 'id' # Use 'id' instead of 'pk'
async def retrieve(self, request, id: int):
"""GET /articles/{id}"""
article = await self.get_object(id=id)
return {"id": article.id, "title": article.title}
@action(methods=["POST"], detail=True)
async def feature(self, request, id: int):
"""POST /articles/{id}/feature"""
return {"featured": True, "article_id": id}
Important: Actions require api.viewset()¶
The @action decorator only works with api.viewset(), not api.view():
# CORRECT: Use api.viewset()
@api.viewset("/articles")
class ArticleViewSet(ViewSet):
@action(methods=["POST"], detail=False)
async def custom_action(self, request):
return {"ok": True}
# WRONG: Will raise ValueError
@api.view("/articles", methods=["GET"])
class ArticleView(ViewSet):
@action(methods=["POST"], detail=False) # Error!
async def custom_action(self, request):
return {"ok": True}
Mixins¶
Use mixins to compose functionality:
from django_bolt.views import (
ListMixin,
RetrieveMixin,
CreateMixin,
UpdateMixin,
DestroyMixin,
ViewSet,
)
# Read-only viewset
@api.viewset("/readonly-items")
class ReadOnlyItemViewSet(ListMixin, RetrieveMixin, ViewSet):
queryset = Item.objects.all()
# Full CRUD viewset
@api.viewset("/items")
class ItemViewSet(
ListMixin, RetrieveMixin, CreateMixin, UpdateMixin, DestroyMixin, ViewSet
):
queryset = Item.objects.all()
Available mixins:
| Mixin | Action | URL | Method |
|---|---|---|---|
ListMixin |
list |
/items |
GET |
RetrieveMixin |
retrieve |
/items/{pk} |
GET |
CreateMixin |
create |
/items |
POST |
UpdateMixin |
update |
/items/{pk} |
PUT |
PartialUpdateMixin |
partial_update |
/items/{pk} |
PATCH |
DestroyMixin |
destroy |
/items/{pk} |
DELETE |
ReadOnlyModelViewSet¶
A convenient shortcut for read-only access:
from django_bolt.views import ReadOnlyModelViewSet
@api.viewset("/public-articles")
class PublicArticleViewSet(ReadOnlyModelViewSet):
queryset = Article.objects.filter(published=True)
This provides only list and retrieve actions.
Sync handlers¶
ViewSets also support synchronous handlers:
@api.view("/sync-resource")
class SyncResourceView(APIView):
def get(self, request): # Note: not async
return {"sync": True}
Sync handlers are automatically wrapped to run in a thread pool.