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:
接著,將 rest_framework 加入 INSTALLED_APPS:
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:
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。這個裝飾器會:
- 將 Django 的
HttpRequest包裝成 DRF 的Request物件 - 讓你的 View 能夠回傳
Response物件 - 提供自動的內容協商(Content Negotiation),可以回傳 JSON 或可瀏覽的 HTML 介面
- 根據你指定的 HTTP 方法自動處理不支援的請求
接下來,建立 API 的 URL 設定檔案 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:
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 來改寫前面的範例:
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 設定來使用這些類別:
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 的主要功能:
- 序列化(Serialization):將 Python 物件(如 Model instance)轉換成 JSON 等格式
- 反序列化(Deserialization):將 JSON 等格式轉換回 Python 物件
- 驗證(Validation):驗證輸入資料是否符合規範
建立第一個 Serializer¶
讓我們為 Article Model 建立一個 Serializer。先建立一個新檔案 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 功能:
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)
many=True表示要序列化多個物件(QuerySet)data=request.data表示這是要反序列化的輸入資料is_valid()會驗證資料是否符合 Serializer 定義的規則created_by從登入的使用者取得,而不是從輸入資料
這段程式碼可以運作,但你可能注意到我們需要手動處理 create 和 update 的邏輯。這很繁瑣,而且容易出錯。接下來讓我們看看如何改善。
在 Serializer 中實作 create 和 update¶
我們可以在 Serializer 中定義 create() 和 update() 方法,讓 Serializer 自己負責建立和更新物件:
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 可以簡化成:
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)
- 透過
save()的參數傳入額外的資料,這些資料會被加入validated_data中 - 當同時傳入 instance 和 data 時,
save()會呼叫update()方法
使用 ModelSerializer 簡化¶
每次都要手動定義欄位、寫 create() 和 update() 方法很麻煩。DRF 提供了 ModelSerializer,它會自動根據 Model 產生欄位:
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 提供了 Mixin 和 GenericAPIView 來解決這個問題。
GenericAPIView 基礎¶
GenericAPIView 是 APIView 的擴充,提供了常用的功能:
queryset:定義要操作的資料集serializer_class:定義要使用的 Serializerget_queryset():取得資料集的方法get_object():取得單一物件的方法(會自動處理 404)get_serializer():取得 Serializer 實例的方法
讓我們先用純 GenericAPIView 來改寫之前的範例:
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)
get_object()會自動根據 URL 中的pk參數從queryset中取得物件,如果找不到會自動回傳 404
使用 GenericAPIView 後,程式碼已經比純 APIView 簡潔了:
- 不需要手動定義
queryset和serializer get_object()會自動處理 404 錯誤get_serializer()會自動建立 Serializer 實例
但我們還是需要在每個方法中寫重複的邏輯。接下來讓我們看看 Mixin 如何進一步簡化。
Mixin 類別¶
DRF 提供了以下 Mixin:
| Mixin | 提供的方法 | 用途 |
|---|---|---|
ListModelMixin |
list() |
列出多個物件 |
CreateModelMixin |
create() |
建立新物件 |
RetrieveModelMixin |
retrieve() |
取得單一物件 |
UpdateModelMixin |
update() |
更新物件 |
DestroyModelMixin |
destroy() |
刪除物件 |
讓我們用 Mixin 來改寫 View:
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,我們的程式碼可以簡化成:
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 基礎¶
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¶
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 的預設設定:
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:
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:
接著,將取得 Token 的 API 路徑引入到 url 中:
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 的效果
現在使用者可以用帳號密碼取得 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¶
如果你想要更細緻的權限控制,可以使用 DjangoModelPermissions 或 DjangoModelPermissionsOrAnonReadOnly。這會根據 Django 內建的權限系統(add、change、delete、view)來決定使用者能做什麼操作。
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權限:可以 POSTchange_article權限:可以 PUT/PATCHdelete_article權限:可以 DELETEview_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 文件。
安裝和設定¶
將 drf_spectacular 加入 INSTALLED_APPS:
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 生成器:
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 文件的基本資訊:
SPECTACULAR_SETTINGS = {
"TITLE": "Blog API",
"DESCRIPTION": "Django 大冒險的部落格 API",
"VERSION": "1.0.0",
}
設定 API 文件的 URL¶
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:全文搜尋¶
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)
- 指定要搜尋的欄位
現在可以用 ?search= 參數來搜尋:
OrderingFilter:排序¶
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)
- 指定允許排序的欄位
使用 ?ordering= 參數來排序,前面加 - 表示降序:
django-filter:進階過濾¶
對於更複雜的過濾需求,我們可以使用在任務六中已經安裝過的 django-filter 套件。DRF 可以直接與 django-filter 整合:
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)
- 指定可以用來過濾的欄位
現在可以用欄位名稱直接過濾:
# 只顯示已發布的文章
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 的差異。