Python新手的FastAPI之旅3:FastAPI建構路由

Sean Yeh
Python Everywhere -from Beginner to Advanced
17 min readJul 19, 2023
Heeze, North Brabant, Dutch, photo by Sean Yeh

回顧一下,到目前為止我們已經對FastAPI有了初步的認識,我們曾經介紹了如何輕鬆掌握它的基本特性,並開始FastAPI的學習旅程。

還記得上一篇曾經討論過的說明文件和SwaggerUI的那些重要元素嗎?例如「狀態碼」、「標籤」、「摘要」、「描述」等,這些都是我們一步步探索FastAPI的足跡。

接下來,我們將進入到一個新的領域,那就是「路由」。當我們的應用程式像雪球一樣越滾越大,需要更多更豐富的功能時,「路由」就成了我們的好夥伴,它可以將程式碼切分為各種小模組,如此一來,我們就能更有效地管理程式碼了。

為了讓大家能夠更容易理解路由,我們將會在這篇文章中深入解析,看看「路由」到底是什麼,它在FastAPI裡有哪些不可或缺的角色。此外,我們也會介紹如何在FastAPI裡使用路由,讓您的程式架構更加清晰。最後,當我們的應用程式需要更多的功能,我們又該如何在已有的路由下,增加更多的端點呢?這一切,都會在本篇文章中詳細揭曉。

讓我們一起揭開「路由」的神秘面紗,探索它在FastAPI應用程式中的重要角色。準備好了嗎?讓我們馬上開始吧!

何謂路由

「路由」究竟是什麼?

想像一下現實世界的道路。當我們想要從家裡移動到學校,平時應該會走一條特定的路線,這條路線就像是路由,指引我們如何從一個地點到達另一個地點。在程式設計中,「路由」也是一樣的概念。

在網路的世界中,路由就像是一個路標,它告訴程式應該如何把使用者的請求(request)導向正確的處理程序。換句話說,當您在網頁上點擊一個連結或送出一個表單時,這個請求就會被送到伺服器,然後路由就會根據這個請求來決定該如何處理。

FastAPI中的路由

FastAPI中的路由是透過裝飾器(Decorator)的方式來定義的。裝飾器是Python的一種語法,可以讓工程師在不修改函式定義的情況下,增加函式的功能。在FastAPI中,我們通常會在函式上方加上一行裝飾器,用來定義該函式對應的路由。

舉例來說,假設我們有一個用來顯示首頁的函式,可以寫成下面的程式碼:

@app.get("/")
def read_root():
return {"Hello": "World"}

在這個例子中,@app.get(“/”)就是一個裝飾器,它告訴FastAPI,當使用者造訪網站的根目錄(也就是”/”)時,應該執行read_root這個函式。

所以,當我們談到路由,就是在談論如何將使用者的請求導向正確的處理函式。每個路由都對應到一個特定的函式,而這個函式就是用來處理使用者的請求,並返回結果。

當路由變多時

如果您的API只有少數幾個路由的話,僅透過上面方式處理,放在同一個檔案中,並不會有什麼問題。然而,當您的業務蒸蒸日上,家大業大,服務的端點越來越多的時候,可以想像的是,這個存放路由的檔案,將會越來越膨脹越來越肥大。

因此引申出下一個問題:亦即當API越來越大的時候,該如何處理才可以讓眾多的路由們可以井然有序的排排站?在FastAPI中我們要怎麼來實現這些呢?以下分成兩個步驟來說明。

重構的第一步

以下是拆分路由的方式:可以依照應用程式的功能來拆分路由。例如:我們依照性質將應用程式拆分為blog、news、user、product四個大單元,每個單元裡面又分為兩種不同功能的路由(一種是get,另一種是post)。

以下,將基於上面的假設來說明路由的拆分方式。我們可以透過路由前綴來處理。

以blog來說,有一個叫作「blog」的路由前綴。這表示在這條路由下的所有動作都會以斜線和 'blog' 這個字作為開頭(就是這樣:prefix='/blog')。因此,所有的動作都會被歸在 blog這個路由前綴下。換句話說,上面的blog_get與blog_post都會被歸在blog前綴下。

對此,我們會以下面的程式碼來實現它:

router = APIRouter(prefix='/blog', tags=['blog'])

此外,也可以用標籤(tags=['blog']),再把這些動作分成不同的類別,這樣的分類可以在 SwaggerUI 上面看到。

然後,我們就可以像以前一樣,在這個路由上面定義所有的動作,像這樣:

@route.get('/')

所以,你的程式碼可能會像下面這樣:

from fastapi import APIRouter
router = APIRouter(prefix='/blog', tags=['blog'])

@route.get('/')
def index():
return ....

@route.get('/all_blog')
def get_all_blog():
return ....

要透過網路來造訪這個路由時,需要連的位置已經不是原來的 http://localhost/ 了,而是加上前綴blog的新位置。(前面的localhost可以替換為您的伺服器url位置)

http://localhost/blog/

或者是

http://localhost/blog/all_blog

重構的第二步

完成上面的步驟後,還無法透過網際網路來造訪這些路由。因為這些路由還沒有與我們的主程式聯結在一起。

接下來,我們要把它加入到您的app中。在主程式碼檔案(檔案裡面有宣告FastAPI實例者,例如:main.py裡面有 app = FastAPI() )中,您需要從路由的資料夾中將路由模組匯入。

# 從router中匯入blog
from router import blog

我們在這裡匯入了 'blog' 模組,然後透過 include_router 的方式把它加入到我們的應用程式app中。在這裡,我們的blog router就與主程式完成聯結。

app.include_router(blog.router)

你會看到像下面這樣的程式碼:

from router import blog
app = FastAPI()
app.include_router(blog.router)

透過上述的兩個步驟,就可以完成路由拆分的動作。

實作1:原程式改寫

現在,我們可以用剛才學到的方法,實際改寫自己的程式碼了。把您程式碼中的各個路由,分成不同的檔案和元件。透過這樣的方式,可以更容易地管理您的應用程式。例如,可以把已經建立的相關端點(end point),歸類在同一個資料夾裡面。若採用這樣的結構,檔案管理起來會比較清楚與方便。

1.建立資料夾與檔案

基於這個想法,我們先建立一個router資料夾,在這個資料夾裡面建立第一個檔案blog_get.py。

透過這個方式,未來還可以在同樣的資料夾裡依照不同的功能建立不一樣的檔案,來分別管理。

2.搬移程式碼

接下來,我們把原來放在main.py的檔案中,開頭為get的函式集中到(搬移到)blog_get.py裏面。

# 原來的main.py檔案
from fastapi import FastAPI

app = FastAPI()

# 留下這個路由
@app.get("/")
def index():
return {"Hello": "FastAPI"}

# 需要搬移到blog_get.py之中----
@app.get("/blog/get_all")
...略...

main.py檔案中所有的函式都搬移到blog_get.py之中,只留下一個主要的路由( ‘/’ ):index函式。

到目前為止,這只是暫時的狀態,待會還需要對這個檔案進行連結的處理。

3.匯入APIRouter套件

在搬移程式碼時,我們需要先在目的地檔案(也就是blog_get.py檔案)的前面匯入APIRouter。

from fastapi import APIRouter

匯入APIRouter後,並且需要實體化APIRouter,指定給一個變數(例如:router)。

router = APIRouter()

在這個APIRouter實體中(router),我們可以加入前綴(prefix)與標籤(tags)作為參數。

router = APIRouter(
prefix="/blog",
tags=["blogs"],
)

完成之後,就可以將其他與blog get路由相關的程式碼搬移過來,貼上這裡。

4.修改路由

因為我們已經設定了前綴(prefix)。之後,每次我們使用路由時,會自動替我們加上前綴(prefix)的內容。

因為加了前綴的關係,原本我們在路由中寫的 blog(@app.get("blog/all"))會變成重複的字串@app.get("blogblog/all")。如果不修改原來搬過來的路由,將變成了重複的字串,而無法正常連結,我們必須將它們移除。

另外,在 blog_get.py 裏面,我們必須將原來的 @app.get改為 @route.get。改為使用先前建立的APIRouter實體(route)。

最後,再把原本每一個路由裡面標註的 tags=["blogs"] 移除,因為tag已經在建立APIRouter實體的時候預先設定好了,一樣的標籤不用再重複設定到個別的路由中。

因此,我們把原來寫在個別函式裡面的 tags=["blogs"] 刪除,僅保留get_comment中的comments Tag( tags=["comments"] )。因為這個comments標籤,只存在於get_comment裏面。

至此,與get相關的函式都搬移到這個blog_get.py檔案之中。經過這番搬移後的blog_get.py檔案將如下面的呈現:

from typing import Optional
from fastapi import APIRouter, status, Response
from enum import Enum

router = APIRouter(
prefix="/blog",
tags=["blogs"],
)

# /blog/all
@router.get(
"/all",
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} 筆資料"}

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

class BlogType(str, Enum):
business = "business"
story = "story"
qa = "qa"

# /blog/type/{type}
@router.get("/type/{type}")
def get_blog_type(type: BlogType):
return {"message": f"Blog 的資料型態是 {type}"}

# /blog/{id}
@router.get("/{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}"}

5.與主檔案連結

修改至此,我們若從SwaggerUI檢視,會發現路由只剩下一個(index),原本從SwaggerUI裏面看得到的其他路由,此時都消失了。

這是因為我們把那些路由從main.py 裡面移出去了。單純從main.py看起來,是找不到其他路由的。因此,我們不僅需要處理blog_get.py檔案,還需要將它與main.py進行連結。下面說明連結的方式。

首先,要將blog_get匯入這個檔案中:

from router import blog_get

然後,再透過 include_router 將blog_get中的路由include進來。

app.include_router(blog_get.router)

下面是搬移之後的main.py檔案,這是最後的狀態:

from fastapi import FastAPI
from router import blog_get

app = FastAPI()
app.include_router(blog_get.router)

@app.get("/")
def index():
return {"Hello": "FastAPI"}

6.驗證

修改完畢之後,即可進入SwaggerUI檢視。這時您會發現原本不見的路由,現在都重新出現了。並且分成三個類別blogs、comments與default。

實作2:加入新路由

前面的實作中,我們已經將原有的程式碼改寫完畢。接下來就可以試試看這ˊ種將檔案拆分的狀況,是否真的便於管理?

由於之前改寫的部分是get路由,尚未見建立post相關的路由,假設我們想要加入另外一組post路由。在這樣的架構下,該如何添加新路由?

1.建立檔案

因為建立post路由的目的是要將資料新增進blog中,在功能上雖然與前面的get不同,但性質上仍然屬於blog的一員。因此這個功能仍然要放在以blog為開頭的檔案中。

基於此,我們要在router資料夾裡面增加一個檔案,取名為blog_post.py。

2.匯入APIRouter

接下來,打開 blog_post.py 檔案,與前面 blog_get.py 的做法一樣,在 blog_post.py 裡面匯入APIRouter。

from fastapi import APIRouter

3.建立實體

APIRouter匯入完畢之後,同樣的需要實體化APIRouter,並且加入prefix與tags參數。到目前為止,都與 blog_get.py 的做法一樣。

router = APIRouter(
prefix="/blog",
tags=["blogs"],
)

4.加入路由

接著,就可以在blog_post.py裡面,加入想要設定的post路由。

# blog/new
@router.post('/new')
def create_blog():
程式碼...

5.與主檔案連結

完成路由的設計後,我們需要將這個檔案與主檔案進行連結,這與前面get的做法是一樣的。

連結需要兩個步驟。首先,要在main.py加入下面的程式碼匯入blog_post。

# 從router匯入blog_post
from router import blog_post

接下來,將blog_post include進來,加入app。

# 將blog_post加入app
app.include_router(blog_post.router)

最後,我們可以看到新建立的blog_post.py檔案與修改的main.py 檔案如下:

這是blog_post.py檔案:

from fastapi import APIRouter

router = APIRouter(
prefix="/blog",
tags=["blogs"],
)

@router.post('/new')
def create_blog():
pass

這是main.py檔案:

from fastapi import FastAPI
from router import blog_get
from router import blog_post

app = FastAPI()

# blog_get
app.include_router(blog_get.router)

# blog_post
app.include_router(blog_post.router)


@app.get("/")
def index():
return {"Hello": "FastAPI"}

6.驗證

最後,我們一樣可以透過SwaggerUI檢視修改的結果。這時,您會發現在blogs的分類中,出現了POST項目(下圖綠色的部分)。

結論

透過本篇的學習,我們了解到FastAPI不僅功能強大,而且在架構應用程式上也極具彈性。尤其是藉由「路由」的概念,讓我們可以將龐大的應用程式有效地拆分並管理。就如同一座城市的道路系統,每個路由就如同一條連接各處的道路,讓使用者可以輕易地獲得他們需要的資訊。當我們的應用程式發展到一定規模時,不論是在開發、維護、或者是管理上,路由都能提供非常好的幫助。就像把大城市的地圖會分成各個區域一樣,每一個路由依其性質獨立出來,讓我們能專注於每一個特定區域的開發與改進。

從之前幾篇的FastAPI的基本特性、到SwaggerUI文件部分,再到本篇的路由使用方式,可以看見一個功能強大且易於使用的Python網路框架展現在我們眼前。無論您是初學者,或者是已有豐富的網頁開發經驗者,相信FastAPI都能成為您的好幫手。

--

--

Sean Yeh
Python Everywhere -from Beginner to Advanced

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