iT邦幫忙

2024 iThome 鐵人賽

DAY 13
2
Python

Django 忍法帖——Django Ninja 入門指南系列 第 13

卷 13:回應(一)Django Ninja 處理 HTTP 回應

  • 分享至 

  • xImage
  •  

這一篇要正式進入「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 進行示範(會依需求為它們增補不同內容):

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

開始吧!

一、簡單回應:新增文章

先來看最簡單的回應格式,這個例子會展示如何回應一個 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 的回應。

{
    "id": 666,
    "title": "How to Be a Ninja"
}

為回應加上 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=PostSchema

有了response=PostSchema設定,Django Ninja 會將函式回傳的Post模型物件,丟給PostSchema進行驗證,成功之後直接轉為 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 的常見回應形態,包含多筆資料

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

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

@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 回應:取得文章列表


多重狀態碼回應

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

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

class Token(Schema):
    token: str
    expires: date

class Message(Schema):
    message: str

@api.post('/login', response={200: Token, 401: Message, 402: Message})
...

值得留意的是,字典的 key 不可重複,但值可以!——Message出現了兩次。

但我覺得這個「多重狀態碼回應」設定在實務上沒有很實用,為何?我們後續再談。


小結

本文中,我們從最簡單的回應開始,逐步介紹了如何在回應中返回單一和多筆資料,並提到了 Django Ninja 如何設定多重狀態碼回應。

下一篇將探討,如何處理回應中複雜的巢狀結構,讓我們的 API 愈來愈健全。

本文同步發表於我的部落格——Code and Me


上一篇
卷 12:請求(四)Request Body 與 Schema 介紹
下一篇
卷 14:回應(二)用 Schema 建立巢狀結構回應
系列文
Django 忍法帖——Django Ninja 入門指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
gbaian10
iT邦新手 5 級 ‧ 2024-09-25 22:28:11

如果裝飾器上的 response 引數沒填,會用 type hint 的值來驗證嗎? FastAPI 是會,Ninja 我沒讀文件則不確定。(不過目前看到這裡為止,Ninja 真的跟 FastAPI 超像!)

「多重狀態碼回應」很沒用的原因我來猜一下。
感覺就是 call API 的人多半不在乎 200 以外的內容為何,或者更正確地說,是對方的程式只要知道失敗就好,這些 2XX 以外的詳細錯誤訊息多半是人看而不是機器看的。
如果每種錯誤格式都不一樣,每種都要寫個特殊的 schema 來接收這些結果但卻幾乎都用不到就真的沒什用XD

Kyo Huang iT邦新手 4 級 ‧ 2024-09-26 16:48:05 檢舉

如果裝飾器上的 response 引數沒填,會用 type hint 的值來驗證嗎?

我還真不確定(沒想過),但我認為應該不會,畢竟很多人很多時候,根本沒有寫 return 的 type hints

讓「多重狀態碼回應」用法變得雞肋的原因,一言難盡,牽涉到 Django Ninja 的錯誤處理方式,還有大量使用裝飾器帶來的影響。我會試著在後續文章中說明這一點,只是有點挑戰,哈哈哈

我個人倒覺得 400、404、500 這些區別挺重要,我也很在乎錯誤回應的內容與合理性,只是不會用本文的「多重狀態碼回應」(也就是那個字典)來實踐而已

我要留言

立即登入留言