HTTP 回應總論#

這是 Django Ninja 系列教學的第 13 篇。
這一篇要正式進入「HTTP 回應」環節,也就是第三小節。
本節將透過 4 篇文章,介紹 Django Ninja 如何處理 HTTP 回應:
- 卷 13:回應(一)Django Ninja 處理 HTTP 回應(本文)
- 卷 14:回應(二)用 Schema 建立巢狀結構回應
- 卷 15:回應(三)為何不用 ModelSchema?——相比 DRF,我更偏愛 Django Ninja 的理由
- 卷 16:回應(四)Resolver 方法——欄位資料格式化
我們會講述更多 Schema 用法,透過這些技巧,你能夠精確地控制 API 的輸出格式。無論是單一物件回應,還是複雜的嵌套結構,接下來都會一一提及。
本文所有的程式碼變動,可參考這個 PR。
範例專案#
本文將一步一步,從簡單到複雜,介紹如何透過 Django Ninja 建立 HTTP 回應。
並且用既有的 3 個 API 進行示範(會依需求為它們增補不同內容):
- 新增文章:示範簡單回應,加上狀態碼。
- 取得單一文章:示範單一物件回應,需要 Schema 與定義
response=參數。 - 取得文章列表:示範多個物件回應。
開始吧!
一、簡單回應:新增文章#
先來看最簡單的回應格式,這個例子會展示如何回應一個 Python 字典,並手動設定 HTTP 回應狀態碼。
以「新增文章」API 為例:(省略部分程式碼)
@router.post(path='/posts/')
def create_post(...) -> dict:
...
return {'id': post.id, 'title': post.title}
這裡回應的是一個 Python 字典,事實上,你可以 return「任何能夠 JSON 序列化」的 Python 資料。(所以 Django 模型物件不行,因為它無法直接序列化)
因此,以下這些都可以 return:
- 單純的字串:
"Hello World !" - Python list:
[1 , 2 , 3] - 巢狀的資料結構:
{"name": "Alice", "age": 30, "hobbies": ["reading", "swimming"]}
這些都會被 Django Ninja 自動序列化為 JSON 格式,並作為 API 的回應:
為回應加上 HTTP 狀態碼#
View 函式處理回應,往往要加入 HTTP 狀態碼。尤其在有多種回應狀態的時候,需要透過狀態碼來區分。
做法很簡單,就是在回應的內容前面直接加上:
return 201, {'id': post.id, 'title': post.title}
如此一來,函式的回傳型別就從原來的 dict 變成 tuple 了。
所以我們函式簽名的 type hints 也要跟著修正:
def create_post(...) -> tuple[int, dict]:
如果你沒有加前面這個狀態碼數字,Django Ninja 就將其預設為 200。
值得注意的是,當你的 view 函式要 return「非 200」回應時,必須在 router 裝飾器聲明:
@router.post(path='/posts/', response={201: dict}) # 這裡
response={201: dict} 就是聲明的方式,採用 Python 字典來一一對應狀態碼與回傳內容格式。
創作當時,這部分的範例專案程式碼還未補上,所以這個 API 無法正常回應😅,特此提醒。
上述第一種回應很簡單,不過大部分 API 回應都沒這麼單純。
我們來看第二種回應。
二、單一模型物件回應:取得單一文章#
開發 Django API,回應中的資料,有很大部分是從 Django 模型物件序列化而來。
但通常我們不會直接將資料庫中的所有資訊傳送給前端。相反,我們會進行欄位篩選、驗證或格式轉換。
這樣不僅能夠精確控制 API 的輸出,還能確保資料的正確與安全性。
Django Ninja 中,這些「篩選、驗證、格式轉換」等需求,都是透過 Schema 實現。
我們來為「單得取一文章」API 設計一個回應格式,使用 Schema。
# post/schemas.py
from datetime import datetime
...
class PostResponse(Schema):
id: int
title: str
content: str
author_id: int
created_at: datetime
updated_at: datetime
這個 PostResponse Schema 包含了 Post 幾乎所有的欄位。
注意,Schema 定義將決定輸出的欄位。如果 Schema 中只有 id 一欄,那輸出結果就只會有該欄的資料。
接著,我們在 view 函式中使用這個 Schema:
@router.get(path='/posts/{int:post_id}/', response=PostResponse)
def get_post(request: HttpRequest, post_id: int) -> Post:
"""
取得單一文章
"""
post = Post.objects.get(id=post_id)
return post
只有改一行!——在 router 裝飾器加上 response=PostResponse。
有了 response=PostResponse 設定,Django Ninja 會將函式回傳的 Post 模型物件,丟給 PostResponse 進行驗證,成功之後直接轉為 JSON 格式並送回前端。
看看回應結果:
// http://127.0.0.1:8000/posts/2/
{
"id": 2,
"title": "Alice's Django Ninja Post 1",
"content": "Alice's Django Ninja Post 1 content",
"author_id": 1,
"created_at": "2024-09-12T02:28:16.801Z",
"updated_at": "2024-09-12T02:28:16.801Z"
}
非常好!
三、多個模型物件回應:取得文章列表#
「清單、列表」也是 API 的常見回應形態,包含多筆資料。
我們繼續使用剛剛的 PostResponse,不作任何更動,直接套用在「取得文章列表」這個 API。
一樣,只要更改一行即可,但與前面略有不同:
@router.get(path='/posts/', response=list[PostResponse])
我們使用了 list[PostResponse],表示回應會是一個 PostResponse 物件的 list。
Django Ninja 自動處理 Iterable#
然而實際上,此時你不需要「真的」return 一個 Python list,可以直接回傳 QuerySet 就好,Django Ninja 會自行處理物件的迭代與序列化。
甚至,只要你 return 的是一個 iterable,而且 iterable 中的每一個元素,都能夠通過 PostResponse 驗證(符合格式),那就足夠了!
來看看結果,因為列表太長了,我改用截圖呈現:

多重狀態碼回應#
上面提到的回應,不是 200 就是 201,但通常 API 往往還會有 400、401、403 甚至 500 等回應,如何處理它們之間的對應關係?
沒錯,就是擴大 response= 中的字典!我們直接看官方文件的例子:
class Token(Schema):
token: str
expires: date
class Message(Schema):
message: str
@api.post(
path='/login',
response={200: Token, 401: Message, 402: Message}
)
...
值得留意的是,字典的 key 不可重複,但值可以!——Message 出現了兩次。
但我覺得這個「多重狀態碼回應」設定在實務上沒有很實用,為何?我們後續再談。
小結#
本文中,我們從最簡單的回應開始,逐步介紹了如何在回應中返回單一和多筆資料,並提到了 Django Ninja 如何設定多重狀態碼回應。
下一篇將探討,如何處理回應中複雜的巢狀結構,讓我們的 API 愈來愈健全。