生成客户端

因为 FastAPI 是基于OpenAPI规范的,自然您可以使用许多相匹配的工具,包括自动生成API文档 (由 Swagger UI 提供)。

一个不太明显而又特别的优势是,你可以为你的API针对不同的编程语言生成客户端(有时候被叫做 SDKs )。

OpenAPI 客户端生成

有许多工具可以从OpenAPI生成客户端。

一个常见的工具是 OpenAPI Generator

如果您正在开发前端,一个非常有趣的替代方案是 openapi-typescript-codegen

生成一个 TypeScript 前端客户端

让我们从一个简单的 FastAPI 应用开始:

Python 3.9+Python 3.8+

  1. from fastapi import FastAPI
  2. from pydantic import BaseModel
  3. app = FastAPI()
  4. class Item(BaseModel):
  5. name: str
  6. price: float
  7. class ResponseMessage(BaseModel):
  8. message: str
  9. @app.post("/items/", response_model=ResponseMessage)
  10. async def create_item(item: Item):
  11. return {"message": "item received"}
  12. @app.get("/items/", response_model=list[Item])
  13. async def get_items():
  14. return [
  15. {"name": "Plumbus", "price": 3},
  16. {"name": "Portal Gun", "price": 9001},
  17. ]
  1. from typing import List
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel
  4. app = FastAPI()
  5. class Item(BaseModel):
  6. name: str
  7. price: float
  8. class ResponseMessage(BaseModel):
  9. message: str
  10. @app.post("/items/", response_model=ResponseMessage)
  11. async def create_item(item: Item):
  12. return {"message": "item received"}
  13. @app.get("/items/", response_model=List[Item])
  14. async def get_items():
  15. return [
  16. {"name": "Plumbus", "price": 3},
  17. {"name": "Portal Gun", "price": 9001},
  18. ]

请注意,路径操作 定义了他们所用于请求数据和回应数据的模型,所使用的模型是ItemResponseMessage

API 文档

如果您访问API文档,您将看到它具有在请求中发送和在响应中接收数据的模式(schemas)

生成客户端 - 图1

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

那些信息可以在应用的 OpenAPI模式 被找到,然后显示在API文档中(通过Swagger UI)。

OpenAPI中所包含的模型里有相同的信息可以用于 生成客户端代码

生成一个TypeScript 客户端

现在我们有了带有模型的应用,我们可以为前端生成客户端代码。

安装 openapi-typescript-codegen

您可以使用以下工具在前端代码中安装 openapi-typescript-codegen:

  1. $ npm install openapi-typescript-codegen --save-dev
  2. ---> 100%

生成客户端代码

要生成客户端代码,您可以使用现在将要安装的命令行应用程序 openapi

因为它安装在本地项目中,所以您可能无法直接使用此命令,但您可以将其放在 package.json 文件中。

它可能看起来是这样的:

  1. {
  2. "name": "frontend-app",
  3. "version": "1.0.0",
  4. "description": "",
  5. "main": "index.js",
  6. "scripts": {
  7. "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios"
  8. },
  9. "author": "",
  10. "license": "",
  11. "devDependencies": {
  12. "openapi-typescript-codegen": "^0.20.1",
  13. "typescript": "^4.6.2"
  14. }
  15. }

在这里添加 NPM generate-client 脚本后,您可以使用以下命令运行它:

  1. $ npm run generate-client
  2. frontend-app@1.0.0 generate-client /home/user/code/frontend-app
  3. > openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios

此命令将在 ./src/client 中生成代码,并将在其内部使用 axios(前端HTTP库)。

尝试客户端代码

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

生成客户端 - 图2

您还将自动补全要发送的数据:

生成客户端 - 图3

Tip

请注意, nameprice 的自动补全,是通过其在Item模型(FastAPI)中的定义实现的。

如果发送的数据字段不符,你也会看到编辑器的错误提示:

生成客户端 - 图4

响应(response)对象也拥有自动补全:

生成客户端 - 图5

带有标签的 FastAPI 应用

在许多情况下,你的FastAPI应用程序会更复杂,你可能会使用标签来分隔不同组的路径操作(path operations)

例如,您可以有一个用 items 的部分和另一个用于 users 的部分,它们可以用标签来分隔:

Python 3.9+Python 3.8+

  1. from fastapi import FastAPI
  2. from pydantic import BaseModel
  3. app = FastAPI()
  4. class Item(BaseModel):
  5. name: str
  6. price: float
  7. class ResponseMessage(BaseModel):
  8. message: str
  9. class User(BaseModel):
  10. username: str
  11. email: str
  12. @app.post("/items/", response_model=ResponseMessage, tags=["items"])
  13. async def create_item(item: Item):
  14. return {"message": "Item received"}
  15. @app.get("/items/", response_model=list[Item], tags=["items"])
  16. async def get_items():
  17. return [
  18. {"name": "Plumbus", "price": 3},
  19. {"name": "Portal Gun", "price": 9001},
  20. ]
  21. @app.post("/users/", response_model=ResponseMessage, tags=["users"])
  22. async def create_user(user: User):
  23. return {"message": "User received"}
  1. from typing import List
  2. from fastapi import FastAPI
  3. from pydantic import BaseModel
  4. app = FastAPI()
  5. class Item(BaseModel):
  6. name: str
  7. price: float
  8. class ResponseMessage(BaseModel):
  9. message: str
  10. class User(BaseModel):
  11. username: str
  12. email: str
  13. @app.post("/items/", response_model=ResponseMessage, tags=["items"])
  14. async def create_item(item: Item):
  15. return {"message": "Item received"}
  16. @app.get("/items/", response_model=List[Item], tags=["items"])
  17. async def get_items():
  18. return [
  19. {"name": "Plumbus", "price": 3},
  20. {"name": "Portal Gun", "price": 9001},
  21. ]
  22. @app.post("/users/", response_model=ResponseMessage, tags=["users"])
  23. async def create_user(user: User):
  24. return {"message": "User received"}

生成带有标签的 TypeScript 客户端

如果您使用标签为FastAPI应用生成客户端,它通常也会根据标签分割客户端代码。

通过这种方式,您将能够为客户端代码进行正确地排序和分组:

生成客户端 - 图6

在这个案例中,您有:

  • ItemsService
  • UsersService

客户端方法名称

现在生成的方法名像 createItemItemsPost 看起来不太简洁:

  1. ItemsService.createItemItemsPost({name: "Plumbus", price: 5})

…这是因为客户端生成器为每个 路径操作 使用OpenAPI的内部 操作 ID(operation ID)

OpenAPI要求每个操作 ID 在所有 路径操作 中都是唯一的,因此 FastAPI 使用函数名路径HTTP方法/操作来生成此操作ID,因为这样可以确保这些操作 ID 是唯一的。

但接下来我会告诉你如何改进。 🤓

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

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

在这种情况下,您必须确保每个操作ID在其他方面是唯一的。

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

自定义生成唯一ID函数

FastAPI为每个路径操作使用一个唯一ID,它用于操作ID,也用于任何所需自定义模型的名称,用于请求或响应。

你可以自定义该函数。它接受一个 APIRoute 对象作为输入,并输出一个字符串。

例如,以下是一个示例,它使用第一个标签(你可能只有一个标签)和路径操作名称(函数名)。

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

Python 3.9+Python 3.8+

  1. from fastapi import FastAPI
  2. from fastapi.routing import APIRoute
  3. from pydantic import BaseModel
  4. def custom_generate_unique_id(route: APIRoute):
  5. return f"{route.tags[0]}-{route.name}"
  6. app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
  7. class Item(BaseModel):
  8. name: str
  9. price: float
  10. class ResponseMessage(BaseModel):
  11. message: str
  12. class User(BaseModel):
  13. username: str
  14. email: str
  15. @app.post("/items/", response_model=ResponseMessage, tags=["items"])
  16. async def create_item(item: Item):
  17. return {"message": "Item received"}
  18. @app.get("/items/", response_model=list[Item], tags=["items"])
  19. async def get_items():
  20. return [
  21. {"name": "Plumbus", "price": 3},
  22. {"name": "Portal Gun", "price": 9001},
  23. ]
  24. @app.post("/users/", response_model=ResponseMessage, tags=["users"])
  25. async def create_user(user: User):
  26. return {"message": "User received"}
  1. from typing import List
  2. from fastapi import FastAPI
  3. from fastapi.routing import APIRoute
  4. from pydantic import BaseModel
  5. def custom_generate_unique_id(route: APIRoute):
  6. return f"{route.tags[0]}-{route.name}"
  7. app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
  8. class Item(BaseModel):
  9. name: str
  10. price: float
  11. class ResponseMessage(BaseModel):
  12. message: str
  13. class User(BaseModel):
  14. username: str
  15. email: str
  16. @app.post("/items/", response_model=ResponseMessage, tags=["items"])
  17. async def create_item(item: Item):
  18. return {"message": "Item received"}
  19. @app.get("/items/", response_model=List[Item], tags=["items"])
  20. async def get_items():
  21. return [
  22. {"name": "Plumbus", "price": 3},
  23. {"name": "Portal Gun", "price": 9001},
  24. ]
  25. @app.post("/users/", response_model=ResponseMessage, tags=["users"])
  26. async def create_user(user: User):
  27. return {"message": "User received"}

使用自定义操作ID生成TypeScript客户端

现在,如果你再次生成客户端,你会发现它具有改善的方法名称:

生成客户端 - 图7

正如你所见,现在方法名称中只包含标签和函数名,不再包含URL路径和HTTP操作的信息。

预处理用于客户端生成器的OpenAPI规范

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

我们已经知道该方法与 items 相关,因为它在 ItemsService 中(从标签中获取),但方法名中仍然有标签名作为前缀。😕

一般情况下对于OpenAPI,我们可能仍然希望保留它,因为这将确保操作ID是唯一的

但对于生成的客户端,我们可以在生成客户端之前修改 OpenAPI 操作ID,以使方法名称更加美观和简洁

我们可以将 OpenAPI JSON 下载到一个名为openapi.json的文件中,然后使用以下脚本删除此前缀的标签

  1. import json
  2. from pathlib import Path
  3. file_path = Path("./openapi.json")
  4. openapi_content = json.loads(file_path.read_text())
  5. for path_data in openapi_content["paths"].values():
  6. for operation in path_data.values():
  7. tag = operation["tags"][0]
  8. operation_id = operation["operationId"]
  9. to_remove = f"{tag}-"
  10. new_operation_id = operation_id[len(to_remove) :]
  11. operation["operationId"] = new_operation_id
  12. file_path.write_text(json.dumps(openapi_content))

通过这样做,操作ID将从类似于 items-get_items 的名称重命名为 get_items ,这样客户端生成器就可以生成更简洁的方法名称。

使用预处理的OpenAPI生成TypeScript客户端

现在,由于最终结果保存在文件openapi.json中,你可以修改 package.json 文件以使用此本地文件,例如:

  1. {
  2. "name": "frontend-app",
  3. "version": "1.0.0",
  4. "description": "",
  5. "main": "index.js",
  6. "scripts": {
  7. "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios"
  8. },
  9. "author": "",
  10. "license": "",
  11. "devDependencies": {
  12. "openapi-typescript-codegen": "^0.20.1",
  13. "typescript": "^4.6.2"
  14. }
  15. }

生成新的客户端之后,你现在将拥有清晰的方法名称,具备自动补全错误提示等功能:

生成客户端 - 图8

优点

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

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

你还将获得针对所有内容的错误提示。

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

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

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