DRF Views:APIView 與 ViewSet 完全解析 | Django 教學

2026/06/13 2026/05/24
DRF Views:APIView 與 ViewSet 完全解析 | Django 教學

在前面的章節中,我們學會了如何用 Serializer(序列化器) 定義 API 的資料格式。但光有序列化器還不夠 — 我們還需要一個元件來接收 HTTP 請求、呼叫序列化器處理資料、並回傳適當的回應。這個元件就是 DRF Views(視圖層)。Django REST Framework 提供了從底層的 @api_view 裝飾器、APIView 類別,到高度抽象的 ViewSetModelViewSet 的完整視圖階層,讓你可以根據需求選擇合適的抽象程度。本篇將完整解析這四種寫法的差異、適用場景,並透過同一個 Post API 的四種實作方式做對比。

DRF View 階層總覽

在深入各種 View 之前,先了解 DRF 的 View 繼承階層(View Hierarchy),有助於理解每一層抽象做了什麼事:

Django View
    └── APIView                    ← DRF 基礎,處理認證/權限/Content Negotiation
            ├── GenericAPIView     ← 加入 queryset、serializer_class、分頁等支援
            │       └── ListAPIView / CreateAPIView / ...(Mixin 組合)
            │
            └── ViewSet            ← 將相關 View 組合成一個類別
                    └── GenericViewSet
                            ├── ReadOnlyModelViewSet  (list + retrieve)
                            └── ModelViewSet          (全部 CRUD)
View 類型功能適用場景
@api_view函數式視圖(FBV)裝飾器簡單端點、原型開發
APIView基礎類別式視圖(CBV)完全客製化的邏輯
ViewSet操作集合(搭配 Router)需要 Router 但有客製化需求
ModelViewSet完整 CRUD + Router標準資源 CRUD

從上到下,抽象程度遞增、程式碼量遞減,但客製化彈性也隨之降低。接下來我們逐一介紹。

@api_view 裝飾器(函數式 API 視圖)

@api_view 是 DRF 提供的函數式視圖(Function-Based View, FBV)裝飾器,最適合簡單的 API 端點。它將普通的 Django View 函數包裝成 DRF 視圖,自動處理認證(Authentication)、權限(Permission)與內容協商(Content Negotiation):

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from .models import Post
from .serializers import PostSerializer

@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated])
def post_list(request):
    """
    GET  /api/posts/  → 取得文章列表
    POST /api/posts/  → 建立新文章
    """
    if request.method == 'GET':
        posts = Post.objects.filter(is_published=True)
        serializer = PostSerializer(posts, many=True,
                                    context={'request': request})
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = PostSerializer(data=request.data,
                                    context={'request': request})
        if serializer.is_valid():
            serializer.save(author=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@api_view 接受一個 HTTP 方法列表作為參數(如 ['GET', 'POST']),限制該端點只允許指定的方法。未列出的方法會自動回傳 405 Method Not Allowed

request.data 與 request.query_params

DRF 的 Request 物件與 Django 原生的 HttpRequest 不同,它提供了更強大的請求解析能力:

  • request.data:解析請求本體(Request Body)中的資料,支援 JSON、Form Data、Multipart 等格式,取代了 Django 原生的 request.POSTrequest.FILES 的組合
  • request.query_params:取得 URL 查詢參數(Query Parameters),等同於 request.GET,但名稱更語義化
@api_view(['GET'])
def post_search(request):
    # 取得查詢參數:GET /api/posts/search/?keyword=django&page=2
    keyword = request.query_params.get('keyword', '')
    page = request.query_params.get('page', 1)

    posts = Post.objects.filter(title__icontains=keyword)
    serializer = PostSerializer(posts, many=True)
    return Response(serializer.data)

APIView 類別(類別式 API 視圖)

APIView 是 DRF 中類別式視圖(Class-Based View, CBV)的基礎。它繼承自 Django 的 View,但額外整合了 DRF 的認證、權限、節流(Throttling)與內容協商機制。你可以透過定義 get()post()put()patch()delete() 等方法來處理對應的 HTTP 請求:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Post
from .serializers import PostSerializer

class PostListAPIView(APIView):
    """
    GET  /api/posts/  → 取得文章列表
    POST /api/posts/  → 建立新文章
    """
    def get(self, request):
        posts = Post.objects.filter(is_published=True)
        serializer = PostSerializer(posts, many=True,
                                    context={'request': request})
        return Response(serializer.data)

    def post(self, request):
        serializer = PostSerializer(data=request.data,
                                    context={'request': request})
        if serializer.is_valid():
            serializer.save(author=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class PostDetailAPIView(APIView):
    """
    GET    /api/posts/<pk>/  → 取得單篇文章
    PUT    /api/posts/<pk>/  → 完整更新文章
    PATCH  /api/posts/<pk>/  → 部分更新文章
    DELETE /api/posts/<pk>/  → 刪除文章
    """
    def get_object(self, pk):
        try:
            return Post.objects.get(pk=pk)
        except Post.DoesNotExist:
            return None

    def get(self, request, pk):
        post = self.get_object(pk)
        if post is None:
            return Response(status=status.HTTP_404_NOT_FOUND)
        serializer = PostSerializer(post, context={'request': request})
        return Response(serializer.data)

    def put(self, request, pk):
        post = self.get_object(pk)
        if post is None:
            return Response(status=status.HTTP_404_NOT_FOUND)
        serializer = PostSerializer(post, data=request.data,
                                    context={'request': request})
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def patch(self, request, pk):
        post = self.get_object(pk)
        if post is None:
            return Response(status=status.HTTP_404_NOT_FOUND)
        # partial=True 允許只傳部分欄位
        serializer = PostSerializer(post, data=request.data,
                                    partial=True,
                                    context={'request': request})
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        post = self.get_object(pk)
        if post is None:
            return Response(status=status.HTTP_404_NOT_FOUND)
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Response 物件

DRF 的 Response 物件會根據客戶端的 Accept 標頭自動選擇輸出格式(JSON、Browsable API 等),這就是**內容協商(Content Negotiation)**的機制。你只需傳入 Python 資料結構,DRF 會自動處理序列化:

from rest_framework.response import Response
from rest_framework import status

# 回傳資料(預設 HTTP 200)
return Response(serializer.data)

# 回傳資料並指定狀態碼
return Response(serializer.data, status=status.HTTP_201_CREATED)

# 回傳錯誤訊息
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

# 只回傳狀態碼(無回應本體)
return Response(status=status.HTTP_204_NO_CONTENT)

ViewSet 概念

ViewSet 是 DRF 的核心設計理念之一:將對同一個資源的所有操作(列表、建立、讀取、更新、刪除)集中在一個類別中。ViewSet 不直接定義 get()post() 等方法,而是使用語義化的 action(動作) 名稱:

Action 名稱對應 HTTP 方法說明
listGET(集合)取得資源列表
createPOST建立新資源
retrieveGET(單一)取得單一資源
updatePUT完整更新資源
partial_updatePATCH部分更新資源
destroyDELETE刪除資源

ViewSet vs APIView 比較

比較項目APIViewViewSet
方法命名get(), post(), put()list(), create(), retrieve()
URL 配置手動在 urls.py 定義搭配 Router 自動產生
類別數量通常需要兩個類別(List + Detail)一個類別處理所有操作
適用場景高度客製化端點標準 CRUD 資源
可擴充性透過方法覆寫透過 @action 新增自訂動作

使用基礎的 ViewSet,你需要手動實作每個 action:

from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from .models import Post
from .serializers import PostSerializer

class PostViewSet(viewsets.ViewSet):
    """
    手動實作所有 action 的 ViewSet
    """
    def list(self, request):
        """GET /api/posts/"""
        posts = Post.objects.filter(is_published=True)
        serializer = PostSerializer(posts, many=True,
                                    context={'request': request})
        return Response(serializer.data)

    def create(self, request):
        """POST /api/posts/"""
        serializer = PostSerializer(data=request.data,
                                    context={'request': request})
        if serializer.is_valid():
            serializer.save(author=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def retrieve(self, request, pk=None):
        """GET /api/posts/<pk>/"""
        try:
            post = Post.objects.get(pk=pk)
        except Post.DoesNotExist:
            return Response(status=status.HTTP_404_NOT_FOUND)
        serializer = PostSerializer(post, context={'request': request})
        return Response(serializer.data)

    def update(self, request, pk=None):
        """PUT /api/posts/<pk>/"""
        try:
            post = Post.objects.get(pk=pk)
        except Post.DoesNotExist:
            return Response(status=status.HTTP_404_NOT_FOUND)
        serializer = PostSerializer(post, data=request.data,
                                    context={'request': request})
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def destroy(self, request, pk=None):
        """DELETE /api/posts/<pk>/"""
        try:
            post = Post.objects.get(pk=pk)
        except Post.DoesNotExist:
            return Response(status=status.HTTP_404_NOT_FOUND)
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

ModelViewSet:自動提供完整 CRUD

如果每次都要手動寫 list()create()retrieve() 等方法,那 ViewSet 就沒什麼優勢了。ModelViewSet 的強大之處在於:只要指定 querysetserializer_class,它就會自動提供完整的 CRUD 操作:

from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer

class PostModelViewSet(viewsets.ModelViewSet):
    """
    只需兩行設定,自動提供 list / create / retrieve / update / partial_update / destroy
    """
    queryset = Post.objects.select_related('author').order_by('-created_at')
    serializer_class = PostSerializer

沒錯,就這麼簡單。這三行程式碼等同於前面 ViewSet 範例中數十行的手動實作。

覆寫方法(Hooks)

雖然 ModelViewSet 自動處理了大部分邏輯,但你仍然可以透過覆寫特定方法來加入自訂行為:

perform_create() — 在建立資源時注入額外邏輯:

class PostModelViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.select_related('author').order_by('-created_at')
    serializer_class = PostSerializer

    def perform_create(self, serializer):
        """建立文章時自動將當前使用者設為作者"""
        serializer.save(author=self.request.user)

perform_update() — 在更新資源時執行額外邏輯:

    def perform_update(self, serializer):
        """更新時檢查發布狀態變更,若從草稿變為發布則發送通知"""
        old_published = serializer.instance.is_published
        post = serializer.save()
        if not old_published and post.is_published:
            notify_subscribers.delay(post.id)  # 非同步通知

get_queryset() — 根據請求動態過濾查詢集:

    def get_queryset(self):
        """管理員可看全部,一般使用者只看自己的 + 已發布的"""
        user = self.request.user
        if user.is_staff:
            return Post.objects.all()

        from django.db.models import Q
        return Post.objects.filter(
            Q(author=user) | Q(is_published=True)
        )

get_serializer_class() — 根據 action 選擇不同的序列化器:

    def get_serializer_class(self):
        """列表用輕量版序列化器,詳情用完整版"""
        if self.action == 'list':
            return PostListSerializer
        return PostDetailSerializer

@action 裝飾器(自訂動作)

標準的 CRUD 操作不一定能滿足所有需求。例如「發布文章」或「取得熱門文章」這類自訂功能,可以使用 @action 裝飾器為 ViewSet 新增額外的端點。

detail=True vs detail=False

@action 最關鍵的參數是 detail

  • detail=True:動作針對單一物件,URL 包含主鍵(如 /api/posts/1/publish/
  • detail=False:動作針對資源集合,URL 不含主鍵(如 /api/posts/trending/
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status

class PostModelViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.select_related('author').order_by('-created_at')
    serializer_class = PostSerializer

    @action(detail=True, methods=['post'], url_path='publish')
    def publish(self, request, pk=None):
        """
        POST /api/posts/{pk}/publish/
        將指定文章設為已發布
        """
        post = self.get_object()  # 自動處理 404
        if post.author != request.user:
            return Response(
                {'error': '只有作者可以發布文章'},
                status=status.HTTP_403_FORBIDDEN,
            )
        post.is_published = True
        post.save()
        serializer = self.get_serializer(post)
        return Response(serializer.data)

    @action(detail=False, methods=['get'], url_path='trending')
    def trending(self, request):
        """
        GET /api/posts/trending/
        取得熱門文章 Top 10
        """
        trending_posts = self.get_queryset().filter(
            is_published=True,
        ).order_by('-view_count')[:10]
        serializer = self.get_serializer(trending_posts, many=True)
        return Response(serializer.data)

自訂 URL name 與 methods

@action 還支援更細緻的配置:

@action(
    detail=True,
    methods=['post', 'delete'],  # 允許多個 HTTP 方法
    url_path='bookmark',         # 自訂 URL 路徑片段
    url_name='bookmark',         # 自訂 URL name(用於 reverse())
)
def bookmark(self, request, pk=None):
    """
    POST   /api/posts/{pk}/bookmark/  → 加入書籤
    DELETE /api/posts/{pk}/bookmark/  → 移除書籤
    """
    post = self.get_object()
    if request.method == 'POST':
        request.user.bookmarks.add(post)
        return Response({'status': '已加入書籤'})
    elif request.method == 'DELETE':
        request.user.bookmarks.remove(post)
        return Response({'status': '已移除書籤'})

搭配 Router 後,@action 產生的 URL name 格式為 {basename}-{url_name},例如上面的 bookmark 動作的 URL name 為 post-bookmark

ReadOnlyModelViewSet

如果你的 API 只需要提供讀取操作(不允許建立、更新、刪除),可以使用 ReadOnlyModelViewSet。它只包含 list()retrieve() 兩個 action:

from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer

class PostReadOnlyViewSet(viewsets.ReadOnlyModelViewSet):
    """
    只提供 GET /api/posts/(列表)和 GET /api/posts/<pk>/(詳情)
    不提供 POST、PUT、PATCH、DELETE
    """
    queryset = Post.objects.filter(is_published=True).order_by('-created_at')
    serializer_class = PostSerializer

這在公開 API(如文章列表、分類查詢)中非常實用,既方便又安全。

Router 配置

ViewSet 通常搭配 Router(路由器) 使用,自動產生 URL 配置:

# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import PostModelViewSet

router = DefaultRouter()
router.register('posts', PostModelViewSet, basename='post')

urlpatterns = [
    path('api/', include(router.urls)),
]

# DefaultRouter 自動產生的 URL:
# GET    /api/posts/              → list
# POST   /api/posts/              → create
# GET    /api/posts/{pk}/         → retrieve
# PUT    /api/posts/{pk}/         → update
# PATCH  /api/posts/{pk}/         → partial_update
# DELETE /api/posts/{pk}/         → destroy
# POST   /api/posts/{pk}/publish/ → publish(@action)
# GET    /api/posts/trending/     → trending(@action)

完整範例:Post API 的四種實作方式對比

為了讓你直觀感受不同抽象層級的差異,以下用同一個 Post 資源的 list(列表)和 create(建立)操作做四種實作方式的對比。

方式一:@api_view(FBV)

# views.py
@api_view(['GET', 'POST'])
def post_list(request):
    if request.method == 'GET':
        posts = Post.objects.filter(is_published=True)
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)
    elif request.method == 'POST':
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(author=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

# urls.py
urlpatterns = [
    path('api/posts/', post_list),
]

方式二:APIView(CBV)

# views.py
class PostListAPIView(APIView):
    def get(self, request):
        posts = Post.objects.filter(is_published=True)
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(author=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

# urls.py
urlpatterns = [
    path('api/posts/', PostListAPIView.as_view()),
]

方式三:ViewSet

# views.py
class PostViewSet(viewsets.ViewSet):
    def list(self, request):
        posts = Post.objects.filter(is_published=True)
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

    def create(self, request):
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(author=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

# urls.py
router = DefaultRouter()
router.register('posts', PostViewSet, basename='post')
urlpatterns = [path('api/', include(router.urls))]

方式四:ModelViewSet

# views.py
class PostModelViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.filter(is_published=True)
    serializer_class = PostSerializer

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

# urls.py
router = DefaultRouter()
router.register('posts', PostModelViewSet, basename='post')
urlpatterns = [path('api/', include(router.urls))]

四種方式比較表

比較項目@api_viewAPIViewViewSetModelViewSet
風格FBVCBVCBVCBV
程式碼量極少
URL 配置手動手動RouterRouter
CRUD 自動化完整
客製化彈性中(透過覆寫)
適用場景簡單/一次性端點客製化邏輯需要 Router標準 CRUD

總結

DRF 的 View 層提供了從底層到高層的完整抽象階梯,讓你可以根據實際需求選擇合適的工具。@api_view 裝飾器適合快速建立簡單的函數式端點;APIView 類別給你對每個 HTTP 方法的完全控制權;ViewSet 將同一資源的操作集中管理並搭配 Router 自動產生 URL;ModelViewSet 則是效率之王,只需指定 querysetserializer_class 就能獲得完整的 CRUD 功能。透過 @action 裝飾器,你還可以為 ViewSet 新增任意自訂端點,而 ReadOnlyModelViewSet 則是公開唯讀 API 的最佳選擇。

在實際專案中,建議的策略是:從 ModelViewSet 開始,如果它能滿足需求就直接使用;遇到需要高度客製化的端點再降級到 APIView@api_view。善用 perform_create()get_queryset()get_serializer_class() 等覆寫方法,可以在享受 ModelViewSet 便利性的同時保有足夠的客製化空間。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。