Python新手的FastAPI之旅2:FastAPI 的操作描述

Sean Yeh
Python Everywhere -from Beginner to Advanced
18 min readJul 10, 2023
Belvedere Schlossgarten, Wien,
Österreich, photo by Sean Yeh

這一篇要來聊聊FastAPI的「操作描述」。說到FastAPI,它的描述和文件部分是它的鮮明特色,能讓我們在使用API時輕鬆愉快。

「API的各個不同操作分別代表什麼意思」?透過描述每個人都能一目了然,理解每一項操作分別代表什麼,就像

在同儕之間溝通一樣,直接又清楚。我們接下來要深入探討怎麼寫這些操作描述,並且將該文件公開給使用我們API的用戶們。

首先會聊到「狀態碼」(status code),狀態碼就像是我們做了什麼事情後,會收到的反饋。只要看到狀態碼,就能知道我們的API操作到底是成功了還是出現了問題。

接著要介紹的是「標籤」(tag),你可以想像它就像是一個個的標籤,讓我們可以對不同的端點(也就是API的各種操作)分門別類的貼上標籤,整齊又方便。

然後我們要談談「摘要」和「描述」(summary and description),就像是每本書的封底簡介一樣,清楚告訴大家這個端點能做什麼,有什麼特色,能提供的資訊是什麼的說明書。

最後要提到的是「回應描述」(response description),就像你點了一份甜點,店家會告訴你這個甜點的成分和口感一樣,回應描述能告訴你,當你使用這個端點,你可以期待收到什麼樣的回應。

就這樣,我們一起探索FastAPI的操作描述,讓API的使用變得更容易,也更有效率。

狀態碼(Status Code)

在使用API的過程中,我們常常會依賴狀態碼來了解該API請求是否成功,或者是否遇到了某些問題。所以說,狀態碼其實就是反映了一個操作結果的指標。

如果你在Google搜尋「HTTP狀態碼」,你會找到一些列出狀態碼的網站(例如MDN網站),這些資源可以讓我們更深入了解狀態碼的意義和用途。

舉例來說,當一個操作完成且沒有任何問題時,我們會得到一個「200」代表「成功回應」的狀態碼。另外,你可能也會遇到「301」的狀態碼,這表示你請求的位址已經被永久地重定向到了一個新的URL。還有些像是鼎鼎有名的「404」找不到資源(404 Not Found)或「500」伺服器內部錯誤(500 Internal Server Error)等等的狀態碼,這些都是API運行過程中可能會出現的情況。

舉例說明

我們在開發API的過程中,常常需要使用到各種不同的狀態碼,讓使用者知道當下的API狀況。那麼,我們就以一個實際的應用範例來看看吧。

假設我們有一個blog的API,其中的一個功能就是用ID去找尋對應的blog文章。為了測試這個功能,我們會希望在找到對應的ID時,回傳一個正常的消息,這個消息裡面會包含該ID所對應的一些詳細資訊。

@app.get("/blog/{id}")
def get_blog(id: int):
return {"message": f"Blog 的 id 是:{id}"}

所以,這個例子是要來嘗試模擬一下,當blog的API的功能成功運作時,該如何回傳結果。同時,我們也希望在未找到對應的特定ID的blog文章時,能夠適當地告知使用者這個情況。

要做到這一點,我們需要提供一個狀態碼的預設的選項。例如,為了清楚的示範,在這裡我們提供「404」找不到資源(404 Not Found)的狀態碼作為預設選項。

首先,在還沒有設定預設狀態碼之前,讓我們來看看這個API原本執行的狀況。以下我們會採用SwaggerUI來測試這個範例。在此我們在ID欄位給一個數字(例如:8)

執行後,可以在SwaggerUI裡看到我們得到了一個狀態碼為200,意味著一切正常。

接下來,我們來嘗試修改一下上面的API,將404狀態碼設定為我們API的預設回傳狀態碼(status_code=404)。然後,我們可以加上一個if判斷式,如果Blog的ID大於5,就返回一個「錯誤」的訊息,告訴使用者「找不到該ID的Blog」。這樣的話,使用者在遇到ID大於5的情況時,就會知道是因為找不到對應的Blog,而不是API出現了問題。

@app.get("/blog/{id}", status_code=404)

def get_blog(id: int):
if id > 5:
return {"error": f"找不到 Blog 的 id :{id}"}
else:
return {"message": f"Blog 的 id 是:{id}"}

現在,我們可以在SwaggerUI中來測試我們剛剛修改過的API。這次,我們在ID欄位輸入8來進行測試。按下執行後,你會看到結果出現了我們設定的預設狀態碼404。

安裝狀態碼套件

對於不太想記住所有HTTP狀態碼的人,或者對於覺得這些狀態碼太多太難記的人,其實有另一種解決方法。

首先,我們可以引入FastAPI的status套件,如下面的Python程式碼:

from fastapi import status

匯入這個套件之後,我們就可以將之前的 status_code=404 改寫成如下所示:

@app.get("/blog/{id}", status_code=status.HTTP_404_NOT_FOUND)
def get_blog(id: int):
if id > 5:
return {"error": f"找不到 Blog 的 id :{id}"}
else:
return {"message": f"Blog 的 id 是:{id}"}

在您的編輯器(例如:VsCode)中,當輸入 status 的時候,它會自動彈出各種狀態碼供您選擇。您可以直接選擇需要的狀態碼,或者可以簡單地搜索它。在這裡,我們將原來寫在程式碼中的404狀態碼替換為 status.HTTP_404_NOT_FOUND

這樣修改之後,再次執行程式,您會發現得到的結果與之前完全一致,這就證明我們的修改是成功的。這種方法可以讓我們更簡單、更方便地使用HTTP狀態碼,而不需要去記住每一個狀態碼都對應什麼含義。

的確,我們的API到目前為止,無論輸入什麼,都會固定回傳狀態碼404,這確實是個需要解決的問題。實際上,我們希望在不同的情況下,API可以回傳不同的狀態碼。

為了實現這個目標,可以將程式碼修改如下:

@app.get("/blog/{id}", status_code=status.HTTP_200_OK)
def get_blog(id: int, response: Response):
if id > 5:
response.status_code = status.HTTP_404_NOT_FOUND
return {"error": f"找不到 Blog 的 id :{id}"}
else:
response.status_code = status.HTTP_200_OK
return {"message": f"Blog 的 id 是:{id}"}

我們透過在response物件上直接設定狀態碼。如果ID大於5,我們就設定狀態碼為404,並回傳錯誤訊息;如果ID小於或等於5,我們則設定狀態碼為200,並回傳Blog的ID。而預設的狀態碼則設置為200。

如此一來,我們的API就可以在不同的情況下,回傳對應的狀態碼和訊息了。大家可以在SwaggerUI中來測試看看。

測試

我們現在可以來試試看這段程式碼的運作情況。當我們在id欄位輸入6並執行時,由於6大於5,所以狀態碼將會顯示404。

然後,當我們將id改為3再執行一次,這次因為3小於或等於5,所以狀態碼將會顯示200。

提供正確的狀態碼在API設計中非常重要,因為當前端或其他使用您的API程式碼收到回應後,他們會依據狀態碼來判斷如何處理回應的內容。透過正確的狀態碼,我們可以讓API的使用者更容易理解和處理API的回應,進而提高我們API的實用性和效率。

標籤(Tags)

在軟體開發中,通常我們會根據其功能和行為將操作分類。常見的操作類別如:建立(Create)、讀取(Read)、更新(Update)、刪除(Delete)等。以上四種操作常被合稱為CRUD操作,是大部分軟體系統的基礎。除此之外,還有其他類型的操作,例如:搜尋(Search)、排序(Sort)等。這些都是非常常見的操作類別。一個好的軟體設計,通常會將這些操作有條理地組織起來,讓整個系統更加結構化和易於維護。

在FastAPI中,我們可以透過在路由裝飾器(route decorators)中加入 tags參數,來給特定的路由加上標籤。這讓我們能在API的文件中,根據標籤對API的各個路由進行有條理的組織和分類。這也意味著我們的使用者可以更容易地找到他們需要的API路由。

舉個例子,我們可能會在一個負責處理部落格文章的路由上加上「blogs」標籤,並在另一個負責處理評論的路由上加上「comments」標籤。程式碼可能會看起來像這樣:

@app.get("/blog/{id}", tags=["blogs"])
def get_blog(id: int):
# …處理部落格文章的程式碼…

@app.post("/comment", tags=["comments"])
def create_comment(comment: Comment):
# …處理評論的程式碼…

在上述程式碼中,tags 參數接受一個字串的列表,代表該路由應該被加入的標籤。一個路由可以有多個標籤,你只需要把所有的標籤都放在同一個列表裡即可。

現在,當你打開FastAPI自動產生的Swagger UI文件時,你會看到所有的路由都根據他們的標籤被整齊地分類在一起。這不僅讓你的API文件看起來更加整潔,也讓使用者能更容易地找到他們需要的路由。

範例:以get_blogs_all為例

以下面get_blogs_all函式為例,我們可以在函式中加上tags 參數,並設置標籤。

@app.get("/blog/all")
def get_blogs_all(page=1, page_size: Optional[int] = None):
return {"message": f"所有的 blogs: 來自第 {page} 頁, 總共有 {page_size} 筆資料"}

由於這個函式主要的功能在於處理與部落格有關的程式碼,我們就可以將它貼上「blogs」的標籤,程式碼修改如下:

@app.get("/blog/all", tags=["blogs"])
def get_blogs_all(page=1, page_size: Optional[int] = None):
return {"message": f"所有的 blogs: 來自第 {page} 頁, 總共有 {page_size} 筆資料"}

在Swagger UI中,你會可以發現出現了blogs的標題,並且 /blog/all 被歸在這個標題下面。

此外,除了放置一個標籤外,也可以在同一個函式中放入複數的標籤。我們在剛剛的基礎上,加上下面的get_comment函式。在這個函式中,你會發現我們放了兩個標籤:blogs與comments。並執行看看。

@app.get("/blog/{id}/comments/{comment_id}", tags=["blogs", "comments"])
def get_comment(
id: int,
comment_id: int,
valid: bool = True,
username: Optional[str] = None
):
return {"message": f"Blog 的 id 是:{id}, comment 的 id 是:{comment_id}, valid 是:{valid}"}

從Swagger UI可以發現:出現blog與comment兩個Tag。

並且您會發現 “/blog/{id}/comments/{comment_id}” 的路由會同時出現在blogs標題下以及comments標題下。

摘要和描述(Summary and Description)

在FastAPI中,我們可以透過在路由裝飾器(decorators)中加入summarydescription參數,來給特定的路由加上摘要(summary)和描述( description)。這讓我們能在API的文件中,更好地解釋每個路由的功能。

比如說,我們可能會在一個負責檢索部落格文章的路由( /blog/{id} )上寫上一個描述該路由功能的摘要和描述。程式碼可能會看起來像這樣:

@app.get("/blog/{id}",
summary="檢索部落格文章",
description="這個API呼叫了獲取指定id的部落格文章。...")
def get_blog(id: int):
# ...處理部落格文章的程式碼...

在上述程式碼中,summary參數是一個簡短的句子,用來描述這個路由的主要功能;description參數則可以是一段更長的文字,用來詳細解釋這個路由的功能、使用方式等等。

另外,你也可以在路由處理函式的文字檔字串(Docstring)中寫上描述,FastAPI會自動把這個文字檔字串作為該路由的描述顯示在文件中:

@app.get("/blog/{id}", summary="檢索部落格文章")
def get_blog(id: int):
"""
這個API呼叫了獲取指定id的部落格文章。...
"""
# ...處理部落格文章的程式碼...

範例:以get_blogs_all為例

我們實際上以get_blogs_all為例,來進行說明。首先,修改get_blogs_all的程式碼,在get_blogs_all函式的裝飾器加上summary與description,並且在後面分別輸入一些字串作為說明這個路由的功能與使用方式等等。

@app.get(
"/blog/all",
tags=["blogs"],
summary="取得所有的 blogs",
description="取得所有的 blogs,並且可以指定分頁的資料",
)
def get_blogs_all(page=1, page_size: Optional[int] = None):
return {"message": f"所有的 blogs: 來自第 {page} 頁, 總共有 {page_size} 筆資料"}

存檔之後,我們到Swagger UI裏面,您會發現我們寫在裝飾器加上的summary與description 文字出現在Swagger UI的文件中。如下圖所示,紅色框框的部分為summary的文字;綠色框框的部分則是description的文字。

範例:以get_comment為例

在下面的程式碼,我們透過 get_comment 為例,示範如何在文字檔字串(Docstring)中寫上描述。

@app.get("/blog/{id}/comments/{comment_id}", tags=["blogs", "comments"])
def get_comment(
id: int,
comment_id: int,
valid: bool = True,
username: Optional[str] = None
summary="Get Comment",
):
"""
取得 blog 的 comment 資料
- **id**: blog 的 id
- **comment_id**: comment 的 id
- **valid**: comment 是否有效
- **username**: 使用者名稱
"""
return {"message": f"Blog 的 id 是:{id}, comment 的 id 是:{comment_id}, valid 是:{valid}"}

同樣的,在存檔之後,我們到Swagger UI裏面檢視結果。

如上圖,顯示在畫面上的綠色框框部分為get_comment程式碼中三個“符號(”””)包裹的文字。

讓我們一起養成一個好習慣吧!那就是在寫程式碼的時候,順手加上一些小摘要和解釋。就像我們會在日記裡畫上心心符號,或在筆記旁邊塗鴉一樣,這樣做能讓我們的程式碼更清楚易懂。

當你使用FastAPI的文件時,你會發現每個路由旁邊都附有你寫的那些摘要和解釋。這不僅讓你的API文件看起來豐富多元,而且還能讓使用者更輕易地理解每個路由的功能,以及如何正確地使用它。

回應描述(Response description)

當我們在建構API的時候,除了告訴使用者該如何送出請求,也可以告訴他們請求送出之後會收到什麼。這就像去餐廳用餐的時候,我們不僅想要知道菜單有什麼,還會想知道這道菜看起來、吃起來會是怎樣的感覺。

為了要做到這點,我們只需要使用一個叫做response_description的標籤,透過這個標籤,就能將我們想回應的內容送出去。

範例:以get_blogs_all為例

試試看吧!假設我們在寫一個獲取所有部落格文章的功能( get_blogs_all ),可以透過response_description寫下「所有的 blogs 資料」這樣的描述。

@app.get(
"/blog/all",
tags=["blogs"],
summary="取得所有的 blogs",
description="取得所有的 blogs,並且可以指定分頁的資料",
response_description="所有的 blogs 資料"
)
def get_blogs_all(page=1, page_size: Optional[int] = None):
return {"message": f"所有的 blogs: 來自第 {page} 頁, 總共有 {page_size} 筆資料"}

完成之後,儲存程式碼,並且重新整理您的Swagger UI文件。然後,您就可以在對應的blog API下看到我們剛才寫的描述了(紅色框框部分)。

這樣一來,使用者就會知道他們呼叫這個API後會得到「所有的 blogs 資料」的部落格文章列表。

這就是我們如何提供訊息的方式,在這裡只是簡單的示範而已,當然,你可以在這裡寫下更多的資訊。

結語

總結來說,FastAPI的操作描述就像是API的嚮導,讓我們在使用API的過程中更加方便。

首先,我們談到「狀態碼」,這就好比向世界丟出一顆石頭,並透過回音來判斷石頭是否擊中目標。狀態碼是我們判斷API操作結果的回饋,直接而準確。

接著,探討到「標籤」。它就像是物品的分類標籤,讓一切都井井有條,使我們能夠輕鬆區分並且找到需要的API操作。還有「摘要」和「描述」,這就好比是一本書的簡介或者書摘一般。它們能讓我們一眼就看出API端點的全貌,迅速了解它的具體功能,以及它的特色和提供的資訊。

最後,我們提到了「回應描述」,這就像是在餐廳點餐時,服務員向我們解釋餐點的口味和成分。回應描述告訴我們,一但我們使用這個API端點時,可以期待得到如何的回應。

透過上述的操作描述,我們可以將使用API的過程變得更清楚、更容易,也更有效率。

--

--

Sean Yeh
Python Everywhere -from Beginner to Advanced

# Taipei, Internet Digital Advertising,透過寫作讓我們回想過去、理解現在並思考未來。並樂於分享,這才是最大贏家。