跳到内容

生成 SDK

因为 FastAPI 基于 OpenAPI 规范,所以其 API 可以用许多工具都能理解的标准格式进行描述。

这使得生成最新的 文档、多语言客户端库 (SDKs) 以及与你的代码保持同步的 测试自动化工作流 变得非常容易。

在本指南中,你将学习如何为你的 FastAPI 后端生成 TypeScript SDK

开源 SDK 生成器

一个通用的选择是 OpenAPI Generator,它支持 多种编程语言,并且可以根据你的 OpenAPI 规范生成 SDK。

对于 TypeScript 客户端Hey API 是一个专门的解决方案,为 TypeScript 生态系统提供了优化的体验。

你可以在 OpenAPI.Tools 上发现更多 SDK 生成器。

提示

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 https://:8000/openapi.json -o src/client

这将在 ./src/client 中生成一个 TypeScript SDK。

你可以学习如何 安装 @hey-api/openapi-ts,并在他们的网站上阅读关于 生成输出 的内容。

使用 SDK

现在你可以导入并使用客户端代码了。它看起来可能是这样的,请注意你获得了方法的自动补全

你还将获得发送负载的自动补全

提示

请注意 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 是唯一的。

但我接下来会向你展示如何改进这一点。 🤓

自定义操作 ID 和更好的方法名称

你可以 修改 这些操作 ID 的 生成方式,使其更简单,并在客户端中拥有 更简单的方法名称

在这种情况下,你必须确保每个操作 ID 以其他方式保持 唯一

例如,你可以确保每个 路径操作 都有一个标签,然后基于 标签路径操作 名称(函数名)来生成操作 ID。

自定义生成唯一 ID 函数

FastAPI 为每个 路径操作 使用一个 唯一 ID,该 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"}

使用自定义操作 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

在生成新客户端后,你现在将拥有 简洁的方法名称,并具备所有的 自动补全内联错误提示 等功能

优势

当使用自动生成的客户端时,你将获得以下功能的 自动补全

  • 方法。
  • 正文中的请求负载、查询参数等。
  • 响应负载。

你还将为所有内容提供 内联错误提示

每当你更新后端代码并 重新生成 前端时,它都会获得作为方法可用的新 路径操作,旧的将被移除,任何其他更改都将反映在生成的代码中。 🤓

这也意味着如果发生更改,它会自动 反映 在客户端代码中。如果你 构建 客户端,在使用的数据有任何 不匹配 时,它会报错。

因此,你可以在开发周期的早期 检测到许多错误,而不是等到错误在生产环境中向最终用户显示后,再去调试问题所在。 ✨