Skip to content

產生 SDK

🌐 AI 與人類共同完成的翻譯

此翻譯由人類指導的 AI 完成。🤝

可能會有對原意的誤解,或讀起來不自然等問題。🤖

你可以透過協助我們更好地引導 AI LLM來改進此翻譯。

英文版

由於 FastAPI 建立在 OpenAPI 規格之上,其 API 能以許多工具都能理解的標準格式來描述。

這讓你能輕鬆產生最新的文件、多語言的用戶端程式庫(SDKs),以及與程式碼保持同步的測試自動化工作流程

在本指南中,你將學會如何為你的 FastAPI 後端產生 TypeScript SDK

開源 SDK 產生器

其中一個相當萬用的選擇是 OpenAPI Generator,它支援多種程式語言,並能從你的 OpenAPI 規格產生 SDK。

針對 TypeScript 用戶端Hey API 是專門打造的解決方案,為 TypeScript 生態系提供最佳化的體驗。

你可以在 OpenAPI.Tools 找到更多 SDK 產生器。

Tip

FastAPI 會自動產生 OpenAPI 3.1 規格,因此你使用的任何工具都必須支援這個版本。

來自 FastAPI 贊助商的 SDK 產生器

本節重點介紹由贊助 FastAPI 的公司提供的創投支持公司維運的解決方案。這些產品在高品質的自動產生 SDK 之外,還提供額外功能整合

透過 ✨ 贊助 FastAPI ✨,這些公司幫助確保框架與其生態系維持健康且永續

他們的贊助也展現對 FastAPI 社群(你)的高度承諾,不僅關心提供優良服務,也支持 FastAPI 作為一個穩健且蓬勃的框架。🙇

例如,你可以嘗試:

其中有些方案也可能是開源或提供免費方案,讓你不需財務承諾就能試用。其他商業的 SDK 產生器也不少,你可以在網路上找到。🤓

建立 TypeScript SDK

先從一個簡單的 FastAPI 應用開始:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


class ResponseMessage(BaseModel):
    message: str


@app.post("/items/", response_model=ResponseMessage)
async def create_item(item: Item):
    return {"message": "item received"}


@app.get("/items/", response_model=list[Item])
async def get_items():
    return [
        {"name": "Plumbus", "price": 3},
        {"name": "Portal Gun", "price": 9001},
    ]

注意這些 路徑操作 為請求與回應的有效載荷定義了所用的模型,使用了 ItemResponseMessage 這兩個模型。

API 文件

如果你前往 /docs,你會看到其中包含了請求要送出的資料與回應接收的資料之結構(schemas)

你之所以能看到這些結構,是因為它們在應用內以模型宣告了。

這些資訊都在應用的 OpenAPI 結構中,並顯示在 API 文件裡。

同樣包含在 OpenAPI 中的模型資訊,也可以用來產生用戶端程式碼

Hey API

當我們有含模型的 FastAPI 應用後,就能用 Hey API 來產生 TypeScript 用戶端。最快的方法是透過 npx:

npx @hey-api/openapi-ts -i http://localhost:8000/openapi.json -o src/client

這會在 ./src/client 產生一個 TypeScript SDK。

你可以在他們的網站了解如何安裝 @hey-api/openapi-ts,以及閱讀產生的輸出內容

使用 SDK

現在你可以匯入並使用用戶端程式碼。大致看起來會像這樣,你會發現方法有自動完成:

你也會對要送出的有效載荷獲得自動完成:

Tip

注意 nameprice 的自動完成,這是由 FastAPI 應用中的 Item 模型所定義。

你在送出的資料上也會看到行內錯誤:

回應物件同樣有自動完成:

含標籤的 FastAPI 應用

在許多情況下,你的 FastAPI 應用會更大,你可能會用標籤將不同群組的 路徑操作 分開。

例如,你可以有一個 items 區塊與另一個 users 區塊,並透過標籤區分:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


class ResponseMessage(BaseModel):
    message: str


class User(BaseModel):
    username: str
    email: str


@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
    return {"message": "Item received"}


@app.get("/items/", response_model=list[Item], tags=["items"])
async def get_items():
    return [
        {"name": "Plumbus", "price": 3},
        {"name": "Portal Gun", "price": 9001},
    ]


@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
    return {"message": "User received"}

使用標籤產生 TypeScript 用戶端

若你為使用標籤的 FastAPI 應用產生用戶端,產生器通常也會依標籤將用戶端程式碼分開。

如此一來,用戶端程式碼就能有條理地正確分組與排列:

在此例中,你會有:

  • ItemsService
  • UsersService

用戶端方法名稱

目前像 createItemItemsPost 這樣的產生方法名稱看起來不太俐落:

ItemsService.createItemItemsPost({name: "Plumbus", price: 5})

……那是因為用戶端產生器對每個 路徑操作 都使用 OpenAPI 內部的操作 ID(operation ID)

OpenAPI 要求每個操作 ID 在所有 路徑操作 之間必須唯一,因此 FastAPI 會用函式名稱路徑HTTP 方法/操作來產生該操作 ID,如此便能確保操作 ID 的唯一性。

接下來我會示範如何把它變得更好看。🤓

自訂 Operation ID 與更好的方法名稱

你可以修改這些操作 ID 的產生方式,讓它們更簡潔,並在用戶端中得到更簡潔的方法名稱

在這種情況下,你需要用其他方式確保每個操作 ID 都是唯一的。

例如,你可以確保每個 路徑操作 都有標籤,接著根據標籤路徑操作名稱(函式名稱)來產生操作 ID。

自訂唯一 ID 產生函式

FastAPI 會為每個 路徑操作 使用一個唯一 ID,它會被用於操作 ID,以及任何請求或回應所需的自訂模型名稱。

你可以自訂該函式。它接收一個 APIRoute 並回傳字串。

例如,下面使用第一個標籤(你通常只會有一個標籤)以及 路徑操作 的名稱(函式名稱)。

接著你可以將這個自訂函式以 generate_unique_id_function 參數傳給 FastAPI

from fastapi import FastAPI
from fastapi.routing import APIRoute
from pydantic import BaseModel


def custom_generate_unique_id(route: APIRoute):
    return f"{route.tags[0]}-{route.name}"


app = FastAPI(generate_unique_id_function=custom_generate_unique_id)


class Item(BaseModel):
    name: str
    price: float


class ResponseMessage(BaseModel):
    message: str


class User(BaseModel):
    username: str
    email: str


@app.post("/items/", response_model=ResponseMessage, tags=["items"])
async def create_item(item: Item):
    return {"message": "Item received"}


@app.get("/items/", response_model=list[Item], tags=["items"])
async def get_items():
    return [
        {"name": "Plumbus", "price": 3},
        {"name": "Portal Gun", "price": 9001},
    ]


@app.post("/users/", response_model=ResponseMessage, tags=["users"])
async def create_user(user: User):
    return {"message": "User received"}

使用自訂 Operation ID 產生 TypeScript 用戶端

現在,如果你再次產生用戶端,會看到方法名稱已改善:

如你所見,方法名稱現在包含標籤與函式名稱,不再包含 URL 路徑與 HTTP 操作的資訊。

為用戶端產生器預處理 OpenAPI 規格

產生的程式碼仍有一些重複資訊

我們已經知道這個方法與 items 相關,因為該字已出現在 ItemsService(取自標籤)中,但方法名稱仍然加上了標籤名稱做前綴。😕

對於 OpenAPI 本身,我們可能仍想保留,因為那能確保操作 ID 是唯一的。

但就產生用戶端而言,我們可以在產生前修改 OpenAPI 的操作 ID,來讓方法名稱更簡潔、更乾淨

我們可以把 OpenAPI JSON 下載到 openapi.json 檔案,然後用像這樣的腳本移除該標籤前綴

import json
from pathlib import Path

file_path = Path("./openapi.json")
openapi_content = json.loads(file_path.read_text())

for path_data in openapi_content["paths"].values():
    for operation in path_data.values():
        tag = operation["tags"][0]
        operation_id = operation["operationId"]
        to_remove = f"{tag}-"
        new_operation_id = operation_id[len(to_remove) :]
        operation["operationId"] = new_operation_id

file_path.write_text(json.dumps(openapi_content))
import * as fs from 'fs'

async function modifyOpenAPIFile(filePath) {
  try {
    const data = await fs.promises.readFile(filePath)
    const openapiContent = JSON.parse(data)

    const paths = openapiContent.paths
    for (const pathKey of Object.keys(paths)) {
      const pathData = paths[pathKey]
      for (const method of Object.keys(pathData)) {
        const operation = pathData[method]
        if (operation.tags && operation.tags.length > 0) {
          const tag = operation.tags[0]
          const operationId = operation.operationId
          const toRemove = `${tag}-`
          if (operationId.startsWith(toRemove)) {
            const newOperationId = operationId.substring(toRemove.length)
            operation.operationId = newOperationId
          }
        }
      }
    }

    await fs.promises.writeFile(
      filePath,
      JSON.stringify(openapiContent, null, 2),
    )
    console.log('File successfully modified')
  } catch (err) {
    console.error('Error:', err)
  }
}

const filePath = './openapi.json'
modifyOpenAPIFile(filePath)

如此一來,操作 ID 會從 items-get_items 之類的字串,變成單純的 get_items,讓用戶端產生器能產生更簡潔的方法名稱。

使用預處理後的 OpenAPI 產生 TypeScript 用戶端

由於最終結果現在是在 openapi.json 檔案中,你需要更新輸入位置:

npx @hey-api/openapi-ts -i ./openapi.json -o src/client

產生新的用戶端後,你現在會得到乾淨的方法名稱,同時保有所有的自動完成行內錯誤等功能:

好處

使用自動產生的用戶端時,你會得到以下項目的自動完成

  • 方法
  • 本文中的請求有效載荷、查詢參數等
  • 回應的有效載荷

你也會對所有內容獲得行內錯誤提示。

而且每當你更新後端程式碼並重新產生前端(用戶端),新的 路徑操作 會以方法形式可用、舊的會被移除,其他任何變更也都會反映到產生的程式碼中。🤓

這也代表只要有任何變更,便會自動反映到用戶端程式碼;而當你建置用戶端時,如果使用的資料有任何不匹配,就會直接報錯。

因此,你能在開發週期的很早期就偵測到許多錯誤,而不必等到錯誤在正式環境的最終使用者那裡才出現,然後才開始追查問題所在。✨