生成客户端¶
由于 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 https://127.0.0.1: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 https://127.0.0.1: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 规范以供客户端生成器使用¶
生成的代码仍然有一些重复的信息。
我们已经知道此方法与项目相关,因为该词在 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"
}
}
生成新客户端后,您现在将拥有简洁的方法名称,以及所有自动完成、内联错误等
优势¶
使用自动生成的客户端时,您将获得以下方面的自动完成功能:
- 方法。
- 正文中的请求有效负载、查询参数等。
- 响应有效负载。
您还将获得所有内容的内联错误。
并且,每当您更新后端代码并重新生成前端时,它都会将任何新的路径操作作为方法提供,删除旧的方法,并且任何其他更改都将反映在生成的代码中。🤓
这也意味着如果某些内容发生更改,它将自动反映在客户端代码中。并且,如果您构建客户端,如果使用的数据有任何不匹配,它将出错。
因此,您将在开发周期的早期检测到许多错误,而不必等到错误在生产环境中显示给最终用户,然后尝试调试问题所在。✨