跳轉到

Django REST Framework

上一個任務中,我們學習了 RESTful API 的設計原則、HTTP 方法和狀態碼。 現在,讓我們來學習如何使用 Django REST Framework 來實際建立 API。

開始之前

任務目標

在這個任務中,你將學習:

  • 了解 Django REST Framework 的特色
  • 使用 Function-based View 建立簡單的 API
  • 使用 APIView 類別改寫 API
  • 理解 Serializer 的用途並實作 CRUD 操作
  • 使用 Mixin 和 GenericAPIView 簡化程式碼
  • 使用 ViewSet 和 Router 進一步簡化
  • 設定 API 的驗證與授權機制
  • 使用 drf-spectacular 自動產生 API 文件
  • 設定搜尋、排序與過濾功能

什麼是 Django REST Framework?

Django REST Framework(簡稱 DRF) 是 Django 生態系中最受歡迎的 API 開發套件。它提供了:

  • 強大的 Serializer 機制,讓你輕鬆將 Model 轉換成 JSON
  • 多種 View 類別,從基礎到進階都有
  • 完整的 驗證與授權 機制
  • 自動產生的 可瀏覽 API 介面
  • 豐富的 過濾、分頁、搜尋 功能

安裝 Django REST Framework

首先,讓我們安裝 DRF:

uv add djangorestframework

接著,將 rest_framework 加入 INSTALLED_APPS

core/settings.py
INSTALLED_APPS = [
    # Django 內建 apps
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # 第三方 apps
    "rest_framework",
    "django_bootstrap5",
    "django_extensions",
    "django_filters",
    # 本地 apps
    "practices",
    "blog",
    # 工具 apps (必須放在最後)
    "django_cleanup.apps.CleanupConfig",
]

建立第一個 API:Function-based View

在開始使用 Model 和 Serializer 之前,讓我們先用最簡單的方式體驗一下 API 的感覺。

檔案組織

為了避免 API 相關的程式碼與之前 Template 的 View 混在一起,我們會把 API 的 View 放在獨立的檔案中。因為之後還會學習 Django Ninja 這個套件,所以我們用 drf_views.py 來命名 DRF 的 API View。

首先,建立一個新檔案 blog/drf_views.py

blog/drf_views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response


@api_view(["GET", "POST"])
def article_list(request):
    """文章列表 API"""
    if request.method == "GET":
        return Response({"message": "文章列表"})
    elif request.method == "POST":
        return Response({"message": "新增文章"}, status=201)


@api_view(["GET", "PUT", "DELETE"])
def article_detail(request, pk):
    """文章詳情 API"""
    if request.method == "GET":
        return Response({"message": f"取得文章 {pk}"})
    elif request.method == "PUT":
        return Response({"message": f"更新文章 {pk}"})
    elif request.method == "DELETE":
        return Response({"message": f"刪除文章 {pk}"}, status=204)

這裡我們使用了 @api_view 裝飾器,它是 DRF 提供的最簡單的方式來建立 API。這個裝飾器會:

  1. 將 Django 的 HttpRequest 包裝成 DRF 的 Request 物件
  2. 讓你的 View 能夠回傳 Response 物件
  3. 提供自動的內容協商(Content Negotiation),可以回傳 JSON 或可瀏覽的 HTML 介面
  4. 根據你指定的 HTTP 方法自動處理不支援的請求

接下來,建立 API 的 URL 設定檔案 blog/drf_urls.py

blog/drf_urls.py
from django.urls import path

from blog import drf_views

app_name = "drf-blog"

urlpatterns = [
    path("articles", drf_views.article_list, name="article-list"),
    path("articles/<int:pk>", drf_views.article_detail, name="article-detail"),
]

最後,在專案的主要 URL 設定中引入這個 API URL:

core/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path, reverse_lazy
from django.views.generic import RedirectView

from core import views

auth_urlpatterns = [
    path(
        "login/",
        auth_views.LoginView.as_view(template_name="registration/login.html"),
        name="login",
    ),
    path(
        "logout/",
        auth_views.LogoutView.as_view(),
        name="logout",
    ),
    path("register/", views.register, name="register"),
    path(
        "password-change/",
        auth_views.PasswordChangeView.as_view(
            template_name="registration/password_change.html",
            success_url=reverse_lazy("auth:password_change_done"),
        ),
        name="password_change",
    ),
    path(
        "password-change/done/",
        auth_views.PasswordChangeDoneView.as_view(
            template_name="registration/password_change_done.html"
        ),
        name="password_change_done",
    ),
    path(
        "password-reset/",
        auth_views.PasswordResetView.as_view(
            template_name="registration/password_reset.html",
            success_url=reverse_lazy("auth:password_reset_done"),
        ),
        name="password_reset",
    ),
    path(
        "password-reset/done/",
        auth_views.PasswordResetDoneView.as_view(
            template_name="registration/password_reset_done.html"
        ),
        name="password_reset_done",
    ),
    path(
        "password-reset/<uidb64>/<token>/",
        auth_views.PasswordResetConfirmView.as_view(
            template_name="registration/password_reset_confirm.html",
            success_url=reverse_lazy("auth:password_reset_complete"),
        ),
        name="password_reset_confirm",
    ),
    path(
        "password-reset/complete/",
        auth_views.PasswordResetCompleteView.as_view(
            template_name="registration/password_reset_complete.html"
        ),
        name="password_reset_complete",
    ),
]

urlpatterns = [
    path("", RedirectView.as_view(pattern_name="blog:article_list"), name="root"),
    path("admin/", admin.site.urls),
    path("practices/", include("practices.urls")),
    path("blog/", include("blog.urls")),
    path("auth/", include((auth_urlpatterns, "auth"))),
    path("api-drf/blog/", include("blog.drf_urls")),
]

if settings.DEBUG:
    from debug_toolbar.toolbar import debug_toolbar_urls

    urlpatterns = [
        *urlpatterns,
        *debug_toolbar_urls(),
        *static(settings.STATIC_URL, document_root=settings.STATIC_ROOT),
        *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
    ]

現在啟動開發伺服器,然後用瀏覽器開啟 http://127.0.0.1:8000/api-drf/blog/articles,你會看到 DRF 提供的可瀏覽 API 介面,這個介面讓你可以直接在瀏覽器中測試 API,非常方便!

你也可以用 curl 或其他 HTTP 工具來測試:

curl http://127.0.0.1:8000/api-drf/blog/articles
# {"message":"文章列表"}

curl -X POST http://127.0.0.1:8000/api-drf/blog/articles
# {"message":"新增文章"}

curl http://127.0.0.1:8000/api-drf/blog/articles/1
# {"message":"取得文章 1"}

使用 APIView 類別改寫

就像 Django 的 View 有 Function-based View(FBV)和 Class-based View(CBV)一樣,DRF 也提供了類別版本的 View。讓我們用 APIView 來改寫前面的範例:

blog/drf_views.py
from rest_framework.response import Response
from rest_framework.views import APIView


class ArticleListAPIView(APIView):
    """文章列表 API"""

    def get(self, request):
        return Response({"message": "文章列表"})

    def post(self, request):
        return Response({"message": "新增文章"}, status=201)


class ArticleDetailAPIView(APIView):
    """文章詳情 API"""

    def get(self, request, pk):
        return Response({"message": f"取得文章 {pk}"})

    def put(self, request, pk):
        return Response({"message": f"更新文章 {pk}"})

    def delete(self, request, pk):
        return Response({"message": f"刪除文章 {pk}"}, status=204)

使用 APIView 的好處是:

  • 不同的 HTTP 方法對應不同的 method,程式碼更清楚
  • 可以使用類別的繼承機制來重用程式碼
  • 可以透過類別屬性來設定權限、認證等

更新 URL 設定來使用這些類別:

blog/drf_urls.py
from django.urls import path

from blog import drf_views

app_name = "drf-blog"

urlpatterns = [
    path("articles", drf_views.ArticleListAPIView.as_view(), name="article-list"),
    path(
        "articles/<int:pk>",
        drf_views.ArticleDetailAPIView.as_view(),
        name="article-detail",
    ),
]

Serializer:資料的序列化與驗證

到目前為止,我們的 API 只是回傳固定的文字訊息。在實際應用中,我們需要將 Model 的資料轉換成 JSON 格式回傳,也需要將使用者送來的 JSON 資料轉換回 Model 物件。這就是 Serializer 的工作。

Serializer 的主要功能:

  1. 序列化(Serialization):將 Python 物件(如 Model instance)轉換成 JSON 等格式
  2. 反序列化(Deserialization):將 JSON 等格式轉換回 Python 物件
  3. 驗證(Validation):驗證輸入資料是否符合規範

建立第一個 Serializer

讓我們為 Article Model 建立一個 Serializer。先建立一個新檔案 blog/serializers.py

blog/serializers.py
from rest_framework import serializers


class ArticleSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=200)
    content = serializers.CharField()
    is_published = serializers.BooleanField(default=False)
    created_by = serializers.PrimaryKeyRelatedField(read_only=True)
    created_at = serializers.DateTimeField(read_only=True)
    updated_at = serializers.DateTimeField(read_only=True)

這裡我們手動定義了每個欄位,讓你了解 Serializer 的基本結構:

  • read_only=True:表示這個欄位只會在輸出時使用,不接受輸入
  • max_length:驗證輸入的最大長度
  • default:當沒有提供值時使用的預設值

關於 created_by

你可能注意到 created_by 被設定為 read_only=True。這是因為建立文章的使用者應該從登入狀態取得,而不是讓使用者自己指定。我們之後會在 View 中處理這個邏輯。

實作 CRUD 操作

現在讓我們更新 API View,使用 Serializer 來實作完整的 CRUD 功能:

blog/drf_views.py
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from blog.models import Article
from blog.serializers import ArticleSerializer


class ArticleListAPIView(APIView):
    """文章列表 API"""

    def get(self, request):
        articles = Article.objects.all()
        serializer = ArticleSerializer(articles, many=True)  # (1)!
        return Response(serializer.data)

    def post(self, request):
        serializer = ArticleSerializer(data=request.data)  # (2)!
        if serializer.is_valid():  # (3)!
            # 手動建立 Article 物件
            article = Article.objects.create(
                title=serializer.validated_data["title"],
                content=serializer.validated_data["content"],
                is_published=serializer.validated_data.get("is_published", False),
                created_by=request.user,  # (4)!
            )
            output_serializer = ArticleSerializer(article)
            return Response(output_serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class ArticleDetailAPIView(APIView):
    """文章詳情 API"""

    def get_object(self, pk):
        return Article.objects.get(pk=pk)

    def get(self, request, pk):
        try:
            article = self.get_object(pk)
        except Article.DoesNotExist:
            return Response(
                {"detail": "找不到該文章"}, status=status.HTTP_404_NOT_FOUND
            )
        serializer = ArticleSerializer(article)
        return Response(serializer.data)

    def put(self, request, pk):
        try:
            article = self.get_object(pk)
        except Article.DoesNotExist:
            return Response(
                {"detail": "找不到該文章"}, status=status.HTTP_404_NOT_FOUND
            )
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid():
            # 手動更新 Article 物件
            article.title = serializer.validated_data["title"]
            article.content = serializer.validated_data["content"]
            article.is_published = serializer.validated_data.get(
                "is_published", article.is_published
            )
            article.save()
            output_serializer = ArticleSerializer(article)
            return Response(output_serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        try:
            article = self.get_object(pk)
        except Article.DoesNotExist:
            return Response(
                {"detail": "找不到該文章"}, status=status.HTTP_404_NOT_FOUND
            )
        article.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
  1. many=True 表示要序列化多個物件(QuerySet)
  2. data=request.data 表示這是要反序列化的輸入資料
  3. is_valid() 會驗證資料是否符合 Serializer 定義的規則
  4. created_by 從登入的使用者取得,而不是從輸入資料

這段程式碼可以運作,但你可能注意到我們需要手動處理 createupdate 的邏輯。這很繁瑣,而且容易出錯。接下來讓我們看看如何改善。

在 Serializer 中實作 create 和 update

我們可以在 Serializer 中定義 create()update() 方法,讓 Serializer 自己負責建立和更新物件:

blog/serializers.py
from rest_framework import serializers

from blog.models import Article


class ArticleSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=200)
    content = serializers.CharField()
    is_published = serializers.BooleanField(default=False)
    created_by = serializers.PrimaryKeyRelatedField(read_only=True)
    created_at = serializers.DateTimeField(read_only=True)
    updated_at = serializers.DateTimeField(read_only=True)

    def create(self, validated_data):
        """建立新的 Article 物件"""
        return Article.objects.create(**validated_data)

    def update(self, instance, validated_data):
        """更新現有的 Article 物件"""
        instance.title = validated_data.get("title", instance.title)
        instance.content = validated_data.get("content", instance.content)
        instance.is_published = validated_data.get("is_published", instance.is_published)
        instance.save()
        return instance

現在 View 可以簡化成:

blog/drf_views.py
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from blog.models import Article
from blog.serializers import ArticleSerializer


class ArticleListAPIView(APIView):
    """文章列表 API"""

    def get(self, request):
        articles = Article.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(created_by=request.user)  # (1)!
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class ArticleDetailAPIView(APIView):
    """文章詳情 API"""

    def get_object(self, pk):
        return Article.objects.get(pk=pk)

    def get(self, request, pk):
        try:
            article = self.get_object(pk)
        except Article.DoesNotExist:
            return Response({"detail": "找不到該文章"}, status=status.HTTP_404_NOT_FOUND)
        serializer = ArticleSerializer(article)
        return Response(serializer.data)

    def put(self, request, pk):
        try:
            article = self.get_object(pk)
        except Article.DoesNotExist:
            return Response({"detail": "找不到該文章"}, status=status.HTTP_404_NOT_FOUND)
        serializer = ArticleSerializer(article, data=request.data)  # (2)!
        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):
        try:
            article = self.get_object(pk)
        except Article.DoesNotExist:
            return Response({"detail": "找不到該文章"}, status=status.HTTP_404_NOT_FOUND)
        article.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
  1. 透過 save() 的參數傳入額外的資料,這些資料會被加入 validated_data
  2. 當同時傳入 instance 和 data 時,save() 會呼叫 update() 方法

使用 ModelSerializer 簡化

每次都要手動定義欄位、寫 create()update() 方法很麻煩。DRF 提供了 ModelSerializer,它會自動根據 Model 產生欄位:

blog/serializers.py
from rest_framework import serializers

from blog.models import Article


class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = [
            "id",
            "title",
            "content",
            "is_published",
            "created_by",
            "created_at",
            "updated_at",
        ]
        read_only_fields = ["created_by", "created_at", "updated_at"]

就這樣!ModelSerializer 會自動:

  • 根據 Model 的欄位定義產生對應的 Serializer 欄位
  • 實作 create()update() 方法
  • 加入適當的驗證規則

fields 的設定

你也可以使用 fields = "__all__" 來包含所有欄位,但這不建議在正式環境中使用,因為可能會不小心暴露敏感資訊。明確列出需要的欄位是比較好的做法。

使用 Mixin 和 GenericAPIView 簡化程式碼

雖然我們已經用 ModelSerializer 簡化了序列化的部分,但 View 中還是有很多重複的程式碼:

  • 取得 QuerySet
  • 取得單一物件並處理 404
  • 驗證資料並儲存
  • 處理錯誤回應

DRF 提供了 MixinGenericAPIView 來解決這個問題。

GenericAPIView 基礎

GenericAPIViewAPIView 的擴充,提供了常用的功能:

  • queryset:定義要操作的資料集
  • serializer_class:定義要使用的 Serializer
  • get_queryset():取得資料集的方法
  • get_object():取得單一物件的方法(會自動處理 404)
  • get_serializer():取得 Serializer 實例的方法

讓我們先用純 GenericAPIView 來改寫之前的範例:

blog/drf_views.py
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response

from blog.models import Article
from blog.serializers import ArticleSerializer


class ArticleListAPIView(GenericAPIView):
    """文章列表 API"""

    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    def get(self, request):
        articles = self.get_queryset()
        serializer = self.get_serializer(articles, many=True)
        return Response(serializer.data)

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


class ArticleDetailAPIView(GenericAPIView):
    """文章詳情 API"""

    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    def get(self, request, pk):
        article = self.get_object()  # (1)!
        serializer = self.get_serializer(article)
        return Response(serializer.data)

    def put(self, request, pk):
        article = self.get_object()
        serializer = self.get_serializer(article, data=request.data)
        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):
        article = self.get_object()
        article.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
  1. get_object() 會自動根據 URL 中的 pk 參數從 queryset 中取得物件,如果找不到會自動回傳 404

使用 GenericAPIView 後,程式碼已經比純 APIView 簡潔了:

  • 不需要手動定義 querysetserializer
  • get_object() 會自動處理 404 錯誤
  • get_serializer() 會自動建立 Serializer 實例

但我們還是需要在每個方法中寫重複的邏輯。接下來讓我們看看 Mixin 如何進一步簡化。

Mixin 類別

DRF 提供了以下 Mixin:

Mixin 提供的方法 用途
ListModelMixin list() 列出多個物件
CreateModelMixin create() 建立新物件
RetrieveModelMixin retrieve() 取得單一物件
UpdateModelMixin update() 更新物件
DestroyModelMixin destroy() 刪除物件

讓我們用 Mixin 來改寫 View:

blog/drf_views.py
from rest_framework import mixins
from rest_framework.generics import GenericAPIView

from blog.models import Article
from blog.serializers import ArticleSerializer


class ArticleListAPIView(
    mixins.ListModelMixin, mixins.CreateModelMixin, GenericAPIView
):
    """文章列表 API"""

    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

    def perform_create(self, serializer):
        """在建立物件時設定 created_by"""
        serializer.save(created_by=self.request.user)


class ArticleDetailAPIView(
    mixins.RetrieveModelMixin,
    mixins.UpdateModelMixin,
    mixins.DestroyModelMixin,
    GenericAPIView,
):
    """文章詳情 API"""

    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)

現在程式碼簡潔多了!但我們還可以更簡化。

使用預先定義好的 Generic View

DRF 預先組合好了常用的 Mixin,讓你可以直接使用:

Generic View 包含的 Mixin 支援的方法
ListAPIView List GET
CreateAPIView Create POST
RetrieveAPIView Retrieve GET
UpdateAPIView Update PUT, PATCH
DestroyAPIView Destroy DELETE
ListCreateAPIView List + Create GET, POST
RetrieveUpdateAPIView Retrieve + Update GET, PUT, PATCH
RetrieveDestroyAPIView Retrieve + Destroy GET, DELETE
RetrieveUpdateDestroyAPIView Retrieve + Update + Destroy GET, PUT, PATCH, DELETE

使用這些 Generic View,我們的程式碼可以簡化成:

blog/drf_views.py
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView

from blog.models import Article
from blog.serializers import ArticleSerializer


class ArticleListAPIView(ListCreateAPIView):
    """文章列表 API"""

    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

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


class ArticleDetailAPIView(RetrieveUpdateDestroyAPIView):
    """文章詳情 API"""

    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

只需要幾行程式碼就完成了完整的 CRUD API!

使用 ViewSet 和 Router

目前我們還是需要維護兩個 View(List 和 Detail)和對應的 URL 設定。DRF 的 ViewSet 可以把這兩個 View 合併成一個類別,再透過 Router 自動產生 URL。

ViewSet 基礎

blog/drf_views.py
from rest_framework.viewsets import ModelViewSet

from blog.models import Article
from blog.serializers import ArticleSerializer


class ArticleViewSet(ModelViewSet):
    """文章 API ViewSet"""

    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

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

ModelViewSet 包含了所有 CRUD 操作:

  • list():GET /api-drf/blog/articles → 文章列表
  • create():POST /api-drf/blog/articles → 建立文章
  • retrieve():GET /api-drf/blog/articles/{id} → 文章詳情
  • update():PUT /api-drf/blog/articles/{id} → 完整更新
  • partial_update():PATCH /api-drf/blog/articles/{id} → 部分更新
  • destroy():DELETE /api-drf/blog/articles/{id} → 刪除文章

使用 Router 自動產生 URL

blog/drf_urls.py
from rest_framework.routers import DefaultRouter

from blog import drf_views

app_name = "drf-blog"

router = DefaultRouter(trailing_slash=False)
router.register("articles", drf_views.ArticleViewSet)

urlpatterns = router.urls

DefaultRouter 會自動產生以下 URL:

URL HTTP 方法 View 方法 名稱
/api-drf/blog/articles GET list article-list
/api-drf/blog/articles POST create article-list
/api-drf/blog/articles/{pk} GET retrieve article-detail
/api-drf/blog/articles/{pk} PUT update article-detail
/api-drf/blog/articles/{pk} PATCH partial_update article-detail
/api-drf/blog/articles/{pk} DELETE destroy article-detail

另外,DefaultRouter 還會在 API 根目錄(/api-drf/blog)產生一個列出所有 API 路徑的頁面。

API 的驗證與授權

目前我們的 API 任何人都可以存取,這在實際應用中是不安全的。DRF 提供了完整的驗證(Authentication)和授權(Permission)機制。

設定全域驗證和權限

首先,在 settings.py 中設定 DRF 的預設設定:

core/settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticatedOrReadOnly",
    ],
}

這個設定表示:

  • 驗證方式:支援 Session(網頁登入)和 Token(API 金鑰)兩種驗證方式
  • 權限規則:未登入的使用者只能讀取(GET),登入後才能進行其他操作

設定 Token 驗證

Token 驗證是 API 常用的驗證方式。首先,將 rest_framework.authtoken 加入 INSTALLED_APPS

core/settings.py
INSTALLED_APPS = [
    # Django 內建 apps
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # 第三方 apps
    "rest_framework",
    "rest_framework.authtoken",
    "django_bootstrap5",
    "django_extensions",
    "django_filters",
    # 本地 apps
    "practices",
    "blog",
    # 工具 apps (必須放在最後)
    "django_cleanup.apps.CleanupConfig",
]

然後執行 migrate:

uv run manage.py migrate

接著,將取得 Token 的 API 路徑引入到 url 中:

core/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path, reverse_lazy
from django.views.generic import RedirectView
from rest_framework.authtoken.views import obtain_auth_token

from core import views

auth_urlpatterns = [
    path(
        "login/",
        auth_views.LoginView.as_view(template_name="registration/login.html"),
        name="login",
    ),
    path(
        "logout/",
        auth_views.LogoutView.as_view(),
        name="logout",
    ),
    path("register/", views.register, name="register"),
    path(
        "password-change/",
        auth_views.PasswordChangeView.as_view(
            template_name="registration/password_change.html",
            success_url=reverse_lazy("auth:password_change_done"),
        ),
        name="password_change",
    ),
    path(
        "password-change/done/",
        auth_views.PasswordChangeDoneView.as_view(
            template_name="registration/password_change_done.html"
        ),
        name="password_change_done",
    ),
    path(
        "password-reset/",
        auth_views.PasswordResetView.as_view(
            template_name="registration/password_reset.html",
            success_url=reverse_lazy("auth:password_reset_done"),
        ),
        name="password_reset",
    ),
    path(
        "password-reset/done/",
        auth_views.PasswordResetDoneView.as_view(
            template_name="registration/password_reset_done.html"
        ),
        name="password_reset_done",
    ),
    path(
        "password-reset/<uidb64>/<token>/",
        auth_views.PasswordResetConfirmView.as_view(
            template_name="registration/password_reset_confirm.html",
            success_url=reverse_lazy("auth:password_reset_complete"),
        ),
        name="password_reset_confirm",
    ),
    path(
        "password-reset/complete/",
        auth_views.PasswordResetCompleteView.as_view(
            template_name="registration/password_reset_complete.html"
        ),
        name="password_reset_complete",
    ),
]

urlpatterns = [
    path("", RedirectView.as_view(pattern_name="blog:article_list"), name="root"),
    path("admin/", admin.site.urls),
    path("practices/", include("practices.urls")),
    path("blog/", include("blog.urls")),
    path("auth/", include((auth_urlpatterns, "auth"))),
    path("api-drf/blog/", include("blog.drf_urls")),
    path("api-drf/token", obtain_auth_token, name="api-token"),
]

if settings.DEBUG:
    from debug_toolbar.toolbar import debug_toolbar_urls

    urlpatterns = [
        *urlpatterns,
        *debug_toolbar_urls(),
        *static(settings.STATIC_URL, document_root=settings.STATIC_ROOT),
        *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
    ]

先試試沒有帶 Token 的效果

curl -X POST http://127.0.0.1:8000/api-drf/blog/articles

現在使用者可以用帳號密碼取得 Token:

curl -X POST http://127.0.0.1:8000/api-drf/token \
  -H "Content-Type: application/json" \
  -d '{"username": "your_username", "password": "your_password"}'

# {"token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"}

拿到 Token 後,在後續的請求中帶上 Authorization header:

curl -X POST http://127.0.0.1:8000/api-drf/blog/articles \
  -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"

使用 DjangoModelPermissions

如果你想要更細緻的權限控制,可以使用 DjangoModelPermissionsDjangoModelPermissionsOrAnonReadOnly。這會根據 Django 內建的權限系統(add、change、delete、view)來決定使用者能做什麼操作。

blog/drf_views.py
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
from rest_framework.viewsets import ModelViewSet

from blog.models import Article
from blog.serializers import ArticleSerializer


class ArticleViewSet(ModelViewSet):
    """文章 API ViewSet"""

    permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

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

這個設定表示:

  • 未登入的使用者可以讀取(GET)
  • 登入的使用者需要有對應的權限才能操作:
    • add_article 權限:可以 POST
    • change_article 權限:可以 PUT/PATCH
    • delete_article 權限:可以 DELETE
    • view_article 權限:可以 GET(通常都有)

JWT 驗證簡介

除了 DRF 內建的 Token 驗證,另一種常見的方式是 JWT(JSON Web Token)。JWT 的特點是:

  • 自包含:Token 本身包含使用者資訊,不需要查資料庫驗證
  • 可設定過期時間:Token 會自動過期,需要定期更新
  • 無狀態:伺服器不需要儲存 Token,適合分散式系統

如果你有興趣使用 JWT,可以參考 djangorestframework-simplejwt 這個套件。由於篇幅限制,這裡就不詳細說明實作方式了。

使用 drf-spectacular 產生 API 文件

良好的 API 文件對於開發者來說非常重要。DRF 搭配 drf-spectacular 套件可以自動產生符合 OpenAPI 3.0 規範的 API 文件。

安裝和設定

uv add drf-spectacular

drf_spectacular 加入 INSTALLED_APPS

core/settings.py
INSTALLED_APPS = [
    # Django 內建 apps
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # 第三方 apps
    "rest_framework",
    "rest_framework.authtoken",
    "drf_spectacular",
    "django_bootstrap5",
    "django_extensions",
    "django_filters",
    # 本地 apps
    "practices",
    "blog",
    # 工具 apps (必須放在最後)
    "django_cleanup.apps.CleanupConfig",
]

設定 DRF 使用 drf-spectacular 作為 Schema 生成器:

core/settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticatedOrReadOnly",
    ],
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

設定 API 文件的基本資訊:

core/settings.py
SPECTACULAR_SETTINGS = {
    "TITLE": "Blog API",
    "DESCRIPTION": "Django 大冒險的部落格 API",
    "VERSION": "1.0.0",
}

設定 API 文件的 URL

core/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path, reverse_lazy
from django.views.generic import RedirectView
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from rest_framework.authtoken.views import obtain_auth_token

from core import views

auth_urlpatterns = [
    path(
        "login/",
        auth_views.LoginView.as_view(template_name="registration/login.html"),
        name="login",
    ),
    path(
        "logout/",
        auth_views.LogoutView.as_view(),
        name="logout",
    ),
    path("register/", views.register, name="register"),
    path(
        "password-change/",
        auth_views.PasswordChangeView.as_view(
            template_name="registration/password_change.html",
            success_url=reverse_lazy("auth:password_change_done"),
        ),
        name="password_change",
    ),
    path(
        "password-change/done/",
        auth_views.PasswordChangeDoneView.as_view(
            template_name="registration/password_change_done.html"
        ),
        name="password_change_done",
    ),
    path(
        "password-reset/",
        auth_views.PasswordResetView.as_view(
            template_name="registration/password_reset.html",
            success_url=reverse_lazy("auth:password_reset_done"),
        ),
        name="password_reset",
    ),
    path(
        "password-reset/done/",
        auth_views.PasswordResetDoneView.as_view(
            template_name="registration/password_reset_done.html"
        ),
        name="password_reset_done",
    ),
    path(
        "password-reset/<uidb64>/<token>/",
        auth_views.PasswordResetConfirmView.as_view(
            template_name="registration/password_reset_confirm.html",
            success_url=reverse_lazy("auth:password_reset_complete"),
        ),
        name="password_reset_confirm",
    ),
    path(
        "password-reset/complete/",
        auth_views.PasswordResetCompleteView.as_view(
            template_name="registration/password_reset_complete.html"
        ),
        name="password_reset_complete",
    ),
]

urlpatterns = [
    path("", RedirectView.as_view(pattern_name="blog:article_list"), name="root"),
    path("admin/", admin.site.urls),
    path("practices/", include("practices.urls")),
    path("blog/", include("blog.urls")),
    path("auth/", include((auth_urlpatterns, "auth"))),
    path("api-drf/blog/", include("blog.drf_urls")),
    path("api-drf/token", obtain_auth_token, name="api-token"),
    # API 文件
    path("api-drf/schema", SpectacularAPIView.as_view(), name="schema"),
    path(
        "api-drf/docs",
        SpectacularSwaggerView.as_view(url_name="schema"),
        name="swagger-ui",
    ),
]

if settings.DEBUG:
    from debug_toolbar.toolbar import debug_toolbar_urls

    urlpatterns = [
        *urlpatterns,
        *debug_toolbar_urls(),
        *static(settings.STATIC_URL, document_root=settings.STATIC_ROOT),
        *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
    ]

現在你可以開啟 http://127.0.0.1:8000/api-drf/docs 來查看互動式的 API 文件

設定搜尋、排序與過濾

最後,讓我們為 API 加上搜尋、排序和過濾功能。

SearchFilter:全文搜尋

blog/drf_views.py
from rest_framework.filters import SearchFilter
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
from rest_framework.viewsets import ModelViewSet

from blog.models import Article
from blog.serializers import ArticleSerializer


class ArticleViewSet(ModelViewSet):
    """文章 API ViewSet"""

    permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [SearchFilter]
    search_fields = ["title", "content"]  # (1)!

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)
  1. 指定要搜尋的欄位

現在可以用 ?search= 參數來搜尋:

curl http://127.0.0.1:8000/api-drf/blog/articles?search=django

OrderingFilter:排序

blog/drf_views.py
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
from rest_framework.viewsets import ModelViewSet

from blog.models import Article
from blog.serializers import ArticleSerializer


class ArticleViewSet(ModelViewSet):
    """文章 API ViewSet"""

    permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [SearchFilter, OrderingFilter]
    search_fields = ["title", "content"]
    ordering_fields = ["created_at", "title"]  # (1)!

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)
  1. 指定允許排序的欄位

使用 ?ordering= 參數來排序,前面加 - 表示降序:

curl http://127.0.0.1:8000/api-drf/blog/articles?ordering=-created_at

django-filter:進階過濾

對於更複雜的過濾需求,我們可以使用在任務六中已經安裝過的 django-filter 套件。DRF 可以直接與 django-filter 整合:

blog/drf_views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
from rest_framework.viewsets import ModelViewSet

from blog.models import Article
from blog.serializers import ArticleSerializer


class ArticleViewSet(ModelViewSet):
    """文章 API ViewSet"""

    permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    search_fields = ["title", "content"]
    ordering_fields = ["created_at", "title"]
    filterset_fields = ["is_published", "author"]  # (1)!

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)
  1. 指定可以用來過濾的欄位

現在可以用欄位名稱直接過濾:

# 只顯示已發布的文章
curl http://127.0.0.1:8000/api-drf/blog/articles?is_published=true

# 只顯示特定作者的文章
curl http://127.0.0.1:8000/api-drf/blog/articles?author=1

# 組合使用
curl http://127.0.0.1:8000/api-drf/blog/articles?is_published=true&ordering=-created_at&search=django

任務結束

完成!

恭喜你完成了這個任務!現在你已經學會:

  • 了解 Django REST Framework 的特色
  • 使用 Function-based View 建立簡單的 API
  • 使用 APIView 類別改寫 API
  • 理解 Serializer 的用途並實作 CRUD 操作
  • 使用 Mixin 和 GenericAPIView 簡化程式碼
  • 使用 ViewSet 和 Router 進一步簡化
  • 設定 API 的驗證與授權機制
  • 使用 drf-spectacular 自動產生 API 文件
  • 設定搜尋、排序與過濾功能

在下一個任務中,我們將學習另一個 Python API 框架——Django Ninja,並比較它與 DRF 的差異。