跳至內容

產生客戶端

由於 FastAPI 基於 OpenAPI 規範,您可以與許多工具自動相容,包括自動 API 文件(由 Swagger UI 提供)。

一個不那麼明顯的特殊優勢是,您可以為您的 API 產生許多不同程式語言客戶端(有時稱為 SDK)。

OpenAPI 客戶端產生器

有許多工具可以從 OpenAPI 產生客戶端。

一個常用的工具是 OpenAPI Generator

如果您正在構建前端,一個非常有趣的替代方案是 openapi-ts

客戶端和 SDK 產生器 - 贊助商

也有一些公司支持的基於 OpenAPI (FastAPI) 的客戶端和 SDK 產生器,在某些情況下,它們除了提供高品質的 SDK/客戶端之外,還可以提供額外功能

其中一些也 ✨ 贊助 FastAPI ✨,這確保了 FastAPI 及其生態系統的持續健康發展

這也顯示了他們對 FastAPI 及其社群(您)的真正承諾,因為他們不僅希望為您提供良好的服務,還希望確保您擁有一個良好且健康的框架,FastAPI。 🙇

例如,您可以嘗試

還有其他幾家公司提供類似的服務,您可以在線上搜尋和查找。 🤓

產生 TypeScript 前端客戶端

讓我們從一個簡單的 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},
    ]
from typing import List

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 文件

如果您前往 API 文件,您將看到它具有在請求中發送和在回應中接收的資料的結構描述

您可以看到這些結構描述,因為它們是在應用程式中使用模型宣告的。

該資訊可在應用程式的 OpenAPI 結構描述中取得,然後顯示在 API 文件中(由 Swagger UI 提供)。

而 OpenAPI 中包含的來自模型的相同資訊,可以用於產生客戶端程式碼

產生 TypeScript 客戶端

現在我們有了帶有模型的應用程式,我們可以為前端產生客戶端程式碼。

安裝 openapi-ts

您可以使用以下指令在您的前端程式碼中安裝 openapi-ts

$ npm install @hey-api/openapi-ts --save-dev

---> 100%

產生客戶端程式碼

要產生客戶端程式碼,您可以使用現在已安裝的命令列應用程式 openapi-ts

因為它是安裝在本地專案中,所以您可能無法直接呼叫該命令,但您可以將它放在您的 package.json 檔案中。

它看起來像這樣

{
  "name": "frontend-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "generate-client": "openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios"
  },
  "author": "",
  "license": "",
  "devDependencies": {
    "@hey-api/openapi-ts": "^0.27.38",
    "typescript": "^4.6.2"
  }
}

在 NPM generate-client 腳本設定好之後,您可以使用以下命令執行它

$ npm run generate-client

frontend-app@1.0.0 generate-client /home/user/code/frontend-app
> openapi-ts --input http://localhost:8000/openapi.json --output ./src/client --client axios

該命令將在 ./src/client 中產生程式碼,並在內部使用 axios(前端 HTTP 函式庫)。

試用客戶端程式碼

現在您可以匯入並使用客戶端程式碼,它看起來像這樣,請注意,您可以使用自動完成方法

您也可以使用自動完成要傳送的有效負載

提示

請注意 nameprice 的自動完成,這是在 FastAPI 應用程式中,在 Item 模型中定義的。

您傳送的資料將會有程式碼錯誤提示

回應物件也會有自動完成

帶有標籤的 FastAPI 應用程式

在許多情況下,您的 FastAPI 應用程式會更大,您可能會使用標籤來區分不同的*路徑操作*群組。

例如,您可以有一個針對**商品**的區段和另一個針對**使用者**的區段,它們可以透過標籤區分

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"}
from typing import List

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**。

OpenAPI 要求每個操作 ID 在所有*路徑操作*中都是唯一的,因此 FastAPI 使用**函式名稱**、**路徑**和**HTTP 方法/操作**來產生該操作 ID,因為這樣它可以確保操作 ID 是唯一的。

但接下來我會告訴您如何改進它。🤓

自訂操作 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"}
from typing import List

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"}

使用自訂操作 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 中,您需要修改 package.json 以使用該本地檔案,例如

{
  "name": "frontend-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "generate-client": "openapi-ts --input ./openapi.json --output ./src/client --client axios"
  },
  "author": "",
  "license": "",
  "devDependencies": {
    "@hey-api/openapi-ts": "^0.27.38",
    "typescript": "^4.6.2"
  }
}

產生新的客戶端後,您現在將擁有簡潔的方法名稱,以及所有自動完成內嵌錯誤等功能。

優點

使用自動產生的客戶端時,您將獲得以下方面的自動完成

  • 方法。
  • 主體中的請求有效負載、查詢參數等。
  • 回應有效負載。

您還將獲得所有內容的內嵌錯誤提示。

而且每當您更新後端程式碼並重新產生前端時,它都會將任何新的路徑操作作為方法提供,移除舊的方法,並且任何其他更改都將反映在產生的程式碼中。🤓

這也意味著,如果發生任何更改,它將自動反映在客戶端程式碼中。如果您建置客戶端,並且使用的數據有任何不匹配,則會出現錯誤。

因此,您將在開發週期的早期發現許多錯誤,而不必等到錯誤出現在生產環境中的最終用戶面前,然後再嘗試除錯問題所在。✨