跳到内容

生成 SDK

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

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

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

开源 SDK 生成器

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

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

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

提示

FastAPI 会自动生成 OpenAPI 3.1 规范,因此您使用的任何工具都必须支持此版本。

来自 FastAPI Sponsor 的 SDK 生成器

本节重点介绍 FastAPI Sponsor 的风险投资支持公司支持的解决方案。这些产品在高质量生成的 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,您将看到其中包含要发送到请求和接收到响应的数据的模式

您可以看到这些模式,因为它们是在应用中使用模型声明的。

该信息可在应用的OpenAPI 模式中找到,然后在 API 文档中显示。

来自模型并包含在 OpenAPI 中的相同信息可用于生成客户端代码

Hey API

一旦我们有了带有模型的 FastAPI 应用,我们就可以使用 Hey API 生成 TypeScript 客户端。最快的方法是通过 npx。

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

这将生成一个 TypeScript SDK 在 ./src/client 目录下。

您可以在他们的网站上了解如何 安装 @hey-api/openapi-ts 并阅读 生成的输出

使用 SDK

现在您可以导入并使用客户端代码。它可以像这样,注意您会获得方法的自动完成功能

您还会获得要发送的载荷的自动完成功能

提示

请注意 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"}

生成带有标签的 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 用于操作 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 规范

生成的代码仍然存在一些重复信息

我们已经知道此方法与项目相关,因为该词包含在 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

生成新客户端后,您将拥有简洁的方法名称,并具有所有自动完成内联错误等功能

优势

使用自动生成的客户端时,您将获得自动完成功能,用于

  • 方法。
  • 请求载荷(在请求体、查询参数等中)。
  • 响应载荷。

您还将获得所有内容的内联错误

并且每当您更新后端代码并重新生成前端时,它都会将任何新的路径操作作为方法提供,旧的则删除,任何其他更改都会反映在生成的代码中。 🤓

这也意味着,如果某些内容发生了更改,它将自动反映在客户端代码中。如果您构建客户端,则在使用数据不匹配时会报错。

因此,您将在开发周期的早期检测到许多错误,而不是等到错误出现在最终用户生产环境中,然后尝试调试问题所在。 ✨