Skip to content

生成客户端

由于 FastAPI 基于 OpenAPI 规范,因此您可以自动兼容许多工具,包括由 Swagger UI 提供的自动 API 文档。

一个不那么显而易见的特定优势是,您可以为您的 API 生成客户端(有时称为 SDKs),适用于许多不同的 编程语言

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 库)。

尝试客户端代码

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

您还将获得要发送的负载的自动补全:

Tip

请注意 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 并输出一个字符串。

例如,这里它使用第一个标签(您可能只有一个标签)和 路径操作 名称(函数名称)。

然后,您可以将该自定义函数传递给 FastAPI 作为 generate_unique_id_function 参数:

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 中(取自标签),但我们仍然在方法名称中带有标签名称前缀。😕

我们可能仍然希望在一般情况下保留它,因为这将确保操作 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_itemsget_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"
  }
}
生成新客户端后,你现在将拥有**简洁的方法名称**,以及所有**自动补全**、**内联错误**等功能:

好处

使用自动生成的客户端时,你将获得以下内容的**自动补全**:

  • 方法。
  • 请求体中的有效载荷、查询参数等。
  • 响应有效载荷。

你还将获得所有内容的**内联错误**。

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

这也意味着如果某些内容发生变化,它将**自动反映**在客户端代码中。如果你**构建**客户端,如果使用的数据有任何**不匹配**,它将报错。

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