Skip to content

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 the get method
  • POST /hello - Calls the post method

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.