2024 iThome 鐵人賽2024 iThome 鐵人賽

這一篇要正式進入「HTTP 回應」環節,也就是第三小節。

本節將透過 4 篇文章,介紹 Django Ninja 如何處理 HTTP 回應

我們會講述更多 Schema 用法,透過這些技巧,你能夠精確地控制 API 的輸出格式。無論是單一物件回應,還是複雜的嵌套結構,接下來都會一一提及。

本文所有的程式碼變動,可參考這個 PR


本文將一步一步,從簡單到複雜,介紹如何透過 Django Ninja 建立 HTTP 回應。

並且用既有的 3 個 API 進行示範(會依需求為它們增補不同內容):

  1. 新增文章:示範簡單回應,加上狀態碼。
  2. 取得單一文章:示範單一物件回應,需要 Schema 與定義response=參數。
  3. 取得文章列表:示範多個物件回應

開始吧!

一、簡單回應:新增文章

先來看最簡單的回應格式,這個例子會展示如何回應一個 Python 字典,並手動設定 HTTP 回應狀態碼。

以「新增文章」API 為例:(省略部分程式碼)

1
2
3
4
@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 的回應:

1
2
3
4
{
"id": 666,
"title": "How to Be a Ninja"
}

為回應加上 HTTP 狀態碼

View 函式處理回應,往往要加入 HTTP 狀態碼。尤其在有多種回應狀態的時候,需要透過狀態碼來區分

做法很簡單,就是在回應的內容前面直接加上:

1
return 201, {'id': post.id, 'title': post.title}

如此一來,函式的回傳型別就從原來的dict變成tuple了。

所以我們函式簽名的 type hints 也要跟著修正:

1
def create_post(...) -> tuple[int, dict]:

如果你沒有加前面這個狀態碼數字,Django Ninja 就將其預設為 200。

值得注意的是,當你的 view 函式要 return「非 200」回應時,必須在router裝飾器聲明:

1
@router.post(path='/posts/', response={201: dict})  # 這裡

response={201: dict}就是聲明的方式,採用 Python 字典來一一對應狀態碼回傳內容格式

創作當時,這部分的範例專案程式碼還未補上,所以這個 API 無法正常回應😅,特此提醒。


上述第一種回應很簡單,不過大部分 API 回應都沒這麼單純

我們來看第二種回應。

二、單一模型物件回應:取得單一文章

開發 Django API,回應中的資料,有很大部分是從 Django 模型物件序列化而來。

但通常我們不會直接將資料庫中的所有資訊傳送給前端。相反,我們會進行欄位篩選、驗證或格式轉換

這樣不僅能夠精確控制 API 的輸出,還能確保資料的正確與安全性。

Django Ninja 中,這些「篩選、驗證、格式轉換」等需求,都是透過 Schema 實現。

我們來為「單得取一文章」API 設計一個回應格式,使用 Schema

1
2
3
4
5
6
7
8
9
10
11
# 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:

1
2
3
4
5
6
7
@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=PostSchema

有了response=PostSchema設定,Django Ninja 會將函式回傳的Post模型物件,丟給PostSchema進行驗證,成功之後直接轉為 JSON 格式並送回前端。

看看回應結果:

1
2
3
4
5
6
7
8
9
// 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 的常見回應形態,包含多筆資料

我們繼續使用剛剛的PostSchema,不作任何更動,直接套用在「取得文章列表」這個 API。

一樣,只要更改一行即可,但與前面略有不同

1
@router.get(path='/posts/', response=list[PostResponse])

我們使用了list[PostSchema],表示回應會是一個PostSchema物件的 list。

Django Ninja 自動處理 Iterable

然而實際上,此時你不需要「真的」return 一個 Python list,可以直接回傳 QuerySet 就好,Django Ninja 會自行處理物件的迭代與序列化

甚至,只要你 return 的是一個 iterable,而且 iterable 中的每一個元素,都能夠通過PostSchema驗證(符合格式),那就足夠了!

來看看結果,因為列表太長了,我改用截圖呈現:

API 回應:取得文章列表API 回應:取得文章列表


多重狀態碼回應

上面提到的回應,不是 200 就是 201,但通常 API 往往還會有 400、401、403 甚至 500 等回應,如何處理它們之間的對應關係

沒錯,就是擴大response=中的字典!我們直接看官方文件的例子:

1
2
3
4
5
6
7
8
9
10
11
12
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 愈來愈健全。