產生客戶端¶
由於 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},
]
請注意,路徑操作使用 Item
和 ResponseMessage
模型定義了它們用於請求有效負載和回應有效負載的模型。
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 函式庫)。
試用客戶端程式碼¶
現在您可以匯入並使用客戶端程式碼,它看起來像這樣,請注意,您可以使用自動完成方法
您也可以使用自動完成要傳送的有效負載
提示
請注意 name
和 price
的自動完成,這是在 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"
}
}
產生新的客戶端後,您現在將擁有簡潔的方法名稱,以及所有自動完成、內嵌錯誤等功能。
優點¶
使用自動產生的客戶端時,您將獲得以下方面的自動完成:
- 方法。
- 主體中的請求有效負載、查詢參數等。
- 回應有效負載。
您還將獲得所有內容的內嵌錯誤提示。
而且每當您更新後端程式碼並重新產生前端時,它都會將任何新的路徑操作作為方法提供,移除舊的方法,並且任何其他更改都將反映在產生的程式碼中。🤓
這也意味著,如果發生任何更改,它將自動反映在客戶端程式碼中。如果您建置客戶端,並且使用的數據有任何不匹配,則會出現錯誤。
因此,您將在開發週期的早期發現許多錯誤,而不必等到錯誤出現在生產環境中的最終用戶面前,然後再嘗試除錯問題所在。✨