跳到内容

生成客户端

由于 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://: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://: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"
  }
}

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

优点

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

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

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

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

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

因此,您将在开发周期的早期就发现许多错误,而不必等待错误在生产环境中向最终用户显示出来,然后再尝试调试问题所在。✨