跳至内容

生成客户端

由于 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},
    ]

请注意,路径操作 定义了它们用于请求有效负载和响应有效负载的模型,使用模型 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 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 库)。

尝试客户端代码

现在您可以导入和使用客户端代码,它可能如下所示,请注意您可以获得方法的自动完成功能

您还可以获得要发送的有效负载的自动完成功能

提示

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

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

然后,您可以将该自定义函数作为 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"
  }
}

生成新客户端后,您现在将拥有简洁的方法名称,以及所有自动完成内联错误

优势

使用自动生成的客户端时,您将获得以下方面的自动完成功能:

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

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

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

这也意味着如果某些内容发生更改,它将自动反映在客户端代码中。并且,如果您构建客户端,如果使用的数据有任何不匹配,它将出错。

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