Python–TypeScript 合约

发布: (2026年2月10日 GMT+8 20:12)
6 分钟阅读
原文: Dev.to

Source: Dev.to

《Python–TypeScript 合约》封面图

nicolas.vbgh

《强制转型传奇》的一部分 — 让 AI 编写高质量代码。

不舒服的局面

后端测试通过。前端测试通过。每一方都能单独工作。但它们能一起工作吗?

  • 后端更改了 API,前端崩溃。没人注意到,直到投入生产。
  • 你把字段从 userName 重命名为 username。后端测试通过。前端测试也通过——它们反正是模拟 API。所有测试都是绿色的。你部署后,生产环境出错,因为前端期待 userName,而后端发送的是 username

这种情况发生的频率比任何人愿意承认的都要高。

Mock 的问题

  • 前端测试会对 API 进行 mock。必须这么做——在每个测试中都无法运行真实的后端。但 mock 会说谎。它们返回你告诉它们返回的内容,而不是后端实际返回的内容。

  • 后端发生变化。Mock 不会。测试通过。生产环境失败。

解决方案: 一个双方必须遵循的唯一真实来源。

OpenAPI 作为契约

FastAPI 会根据你的类型提示自动生成 OpenAPI 规范:

@router.post("/users", response_model=UserResponse)
async def create_user(user: UserCreate) -> User:
    ...

无需额外工作。你的类型会成为规范,规范则成为契约。

{
  "paths": {
    "/users": {
      "post": {
        "requestBody": { "$ref": "#/components/schemas/UserCreate" },
        "responses": {
          "200": { "$ref": "#/components/schemas/UserResponse" }
        }
      }
    }
  }
}

此文件位于代码仓库中。双方必须保持一致。

Orval:生成的 TypeScript 客户端

Orval 读取 OpenAPI 规范并生成 TypeScript。它不仅生成类型——还生成内置 HTTP 层的完整 API 客户端。

配置 (orval.config.ts):

export default defineConfig({
  api: {
    input: '../shared/openapi.json',
    output: {
      target: './src/api/generated/endpoints.ts',
      schemas: './src/api/generated/models',
      client: 'react-query',
      mode: 'tags-split',
    },
  },
});

运行 npx orval,即可得到:

// Generated - don't edit
export const useCreateUser = (
  options?: UseMutationOptions
) => {
  return useMutation({
    mutationFn: (userCreate: UserCreate) => createUser(userCreate),
    ...options,
  });
};

export const createUser = (userCreate: UserCreate): Promise => {
  return customFetch({
    url: '/users',
    method: 'POST',
    data: userCreate,
  });
};

开箱即用的 React Query Hook。支持 mutation、query、缓存失效模式。全部带类型。

你还可以生成:

  • Axios 客户端client: 'axios'
  • Fetch 客户端client: 'fetch'
  • SWR Hookclient: 'swr'
  • Zod schema – 用于在编译时类型之上进行运行时校验

mode: 'tags-split' 选项会为每个 API tag 生成一个文件。实现清晰的分离:users.tsproducts.tsorders.ts。只导入你需要的部分。

无需手动编写 API 客户端。不存在类型漂移。因为类型是直接从规范生成的,所以始终保持一致。

后端把 userName 改名为 username?生成的类型会随之改变。TypeScript 会在前端仍使用旧名称的所有位置报错。你可以在代码上线前先修复这些问题。

工作流程

  1. 后端更改 API。
  2. OpenAPI 规范更新(FastAPI 会自动完成)。
  3. 在前端运行 npm run codegen
  4. 生成的类型更新。
  5. TypeScript 捕获破坏性更改。
  6. 修复前端。
  7. CI 验证所有内容匹配。

没有协调会议。没有 “嘿,你更新前端了吗?” 这种询问。类型强制执行契约。

大门

两个检查。后端规范必须与实际相符。前端类型必须与规范相匹配。

validate:openapi:
  stage: contract
  image: python:3.12-slim
  services:
    - name: postgres:16
      alias: db
  variables:
    DATABASE_URL: postgresql://postgres:postgres@db:5432/test
  before_script:
    - pip install uv && cd backend && uv sync --frozen
  script:
    - cd backend && uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 &
    - sleep 5
    - curl -s http://localhost:8000/openapi.json > /tmp/live-spec.json
    - diff shared/openapi.json /tmp/live-spec.json
  allow_failure: false

validate:codegen:
  stage: contract
  image: node:lts-slim
  before_script:
    - cd frontend && npm ci --prefer-offline
  script:
    - npm run codegen
    - git diff --exit-code src/api/generated
  allow_failure: false
  • validate:openapi – 启动后端,获取实时规范,并将其与提交的规范进行比较。如果两者不同,CI 将失败。说明有人修改了 API 却没有更新规范。
  • validate:codegen – 重新生成 TypeScript 客户端并检查其是否与已提交的代码不同。如果不同,CI 将失败。说明有人更新了规范却没有重新生成客户端。

复制、粘贴、适配。它即可工作。

要点

后端和前端使用不同的语言——这里是 Python,那里是 TypeScript。没有合同,它们会逐渐脱节。小的改动会累积,导致生产环境出错。

OpenAPI 就是合同。它保持双方同步,提前捕获破坏性更改,消除“在我机器上可以运行”的意外。

rval generates the types. CI validates the match.

Breaking changes are impossible to miss. Not "unlikely." Impossible. The pipeline catches them before they ship.

That's the deal.

**Next up:** [E2E Tests] – coming soon – The contract is enforced. Now test the real thing: a browser hitting the full stack.
0 浏览
Back to Blog

相关文章

阅读更多 »

Savior:低层设计

磨练 Go:Low‑Level Design 我回到绘图板上进行面试准备并提升我的问题解决能力。软件开发正处于一个...