定义你的第一个API契约:路径、方法、请求参数、响应体

好,咱们现在开始动手。前面几章聊了数据契约的理念和价值,说实话,光说不练容易飘。这一章,我带你亲手写一个真正的API契约。

你可能会问:「不就是写个接口文档吗?用Word画个表格不就行了?」嗯,我以前也这么干过。结果呢?文档和代码对不上,前端说字段名错了,后端说文档没更新,最后两边一起找我吵架。从那以后,我学乖了——契约必须机器可读、可验证、可执行。

今天我们用OpenAPI 3.0规范,定义一个用户注册接口。别怕,这东西没那么玄乎。

4.1 先搞清楚:契约里到底要写什么?

一个API契约,说白了就是你和调用方之间的「合同」。合同里得写清楚:

  • 路径:这个接口长什么样?比如 /api/v1/users/register
  • 方法:是查数据(GET)、创建数据(POST)、还是改数据(PUT)?
  • 请求参数:调用方需要带什么东西过来?是URL里的参数,还是请求体里的JSON?
  • 响应体:你返回什么给人家?成功长什么样,失败长什么样?

我个人的习惯是,先画一张图,把这几块的关系理清楚。你看下面这张图:

API契约核心要素 客户端 请求(Request) 响应(Response) 服务端 请求包含 • 路径:/api/v1/register • 方法:POST • 参数:JSON body • 请求头:Content-Type 响应包含 • 状态码:201 / 400 • 响应体:JSON • 字段:id, name, email • 错误信息

这张图把一次API调用的完整链路画出来了。客户端发起请求,服务端处理完返回响应。契约就是把这个过程里所有的「约定」白纸黑字写下来。

4.2 路径和方法:先定骨架

好,咱们开始写代码。先搭个骨架:

openapi: 3.0.3
info:
  title: 用户服务API
  version: 1.0.0
  description: 提供用户注册、登录等基础功能

paths:
  /api/v1/users/register:
    post:
      summary: 用户注册
      operationId: registerUser
      tags:
        - 用户管理

这里有几个关键点:

  • 路径:/api/v1/users/register。我习惯把版本号放在路径里,比如 v1。为什么?因为API一升级,旧版本还得继续跑。路径里带版本号,两边不打架。
  • 方法:POST。注册是创建资源,所以用POST。千万别用GET做注册——浏览器一刷新就重复提交,你想想看那画面。
  • operationId:这个字段很多人忽略。但我建议你写上,因为代码生成工具会拿它当函数名。我踩过这个坑,不写的话生成出来的方法名乱七八糟。
小技巧:路径命名用复数名词,比如 /users 而不是 /user。这是RESTful的惯例,大家一看就懂。

4.3 请求参数:把规矩说清楚

骨架搭好了,接下来定义请求参数。注册接口需要用户提供:用户名、邮箱、密码。咱们用 requestBody 来定义:

      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - username
                - email
                - password
              properties:
                username:
                  type: string
                  description: 用户名,3-20个字符
                  minLength: 3
                  maxLength: 20
                  example: zhangsan
                email:
                  type: string
                  format: email
                  description: 用户邮箱
                  example: zhangsan@example.com
                password:
                  type: string
                  format: password
                  description: 密码,至少8位
                  minLength: 8
                  example: "Abc12345"

你看,这里我做了几件事:

  • 用 required 标明哪些字段是必填的。username、email、password,一个都不能少。
  • 用 minLength 和 maxLength 约束长度。用户名3-20位,密码至少8位。这些约束写进契约,调用方一看就知道规矩。
  • 用 format 指定格式。email和password是OpenAPI内置的格式,工具可以自动校验。
  • 用 example 给个示例值。这招特别好用——前端开发拿到契约,直接复制示例就能调接口,省得来回问。
注意:密码字段千万别在响应里返回!契约里只定义请求参数,响应体里不要包含password。我曾经见过一个项目,注册接口返回了密码的哈希值——虽然哈希了,但暴露出去总归不好。

4.4 响应体:成功和失败都要写

请求定义完了,该说响应了。很多新手只写成功响应,失败情况一概不管。结果前端一调接口,返回400了,前端傻眼了:「这啥格式?」

我个人的习惯是:成功响应和错误响应,一个都不能少。

4.4.1 成功响应(201 Created)

      responses:
        '201':
          description: 注册成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: integer
                    description: 业务状态码
                    example: 0
                  message:
                    type: string
                    description: 提示信息
                    example: 注册成功
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        description: 用户ID
                        example: 10001
                      username:
                        type: string
                        description: 用户名
                        example: zhangsan
                      email:
                        type: string
                        description: 邮箱
                        example: zhangsan@example.com
                      createdAt:
                        type: string
                        format: date-time
                        description: 注册时间
                        example: 2025-01-15T10:30:00Z

这里我设计了一个统一的响应结构:code + message + data。为什么这么设计?

  • code:业务状态码。0表示成功,非0表示各种错误。HTTP状态码不够用?业务码来补。
  • message:人类可读的提示信息。前端可以直接弹给用户看。
  • data:真正的业务数据。成功时放数据,失败时可以为null。

这个结构我在好几个项目里用过,前后端都觉得很清爽。你想想看,如果每个接口的响应格式都不一样,前端得写多少适配代码?

4.4.2 错误响应(400 Bad Request)

        '400':
          description: 请求参数错误
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: integer
                    description: 业务状态码
                    example: 1001
                  message:
                    type: string
                    description: 错误描述
                    example: 用户名已存在
                  data:
                    type: object
                    nullable: true
                    description: 错误详情(可选)
                    properties:
                      field:
                        type: string
                        description: 出错的字段
                        example: username
                      reason:
                        type: string
                        description: 具体原因
                        example: 该用户名已被注册

你看,错误响应和成功响应用的是同一个结构。只是 code 变成了非0,data 里放的是错误详情。这样前端处理起来特别统一——先看code,0就走成功逻辑,非0就走错误逻辑。

核心原则:契约里定义的响应体,就是你和调用方之间的「承诺」。承诺越清晰,沟通成本越低。

4.5 完整的契约长什么样?

好,把上面几块拼起来,就是一个完整的API契约了。我直接贴出来:

openapi: 3.0.3
info:
  title: 用户服务API
  version: 1.0.0
  description: 提供用户注册、登录等基础功能

paths:
  /api/v1/users/register:
    post:
      summary: 用户注册
      operationId: registerUser
      tags:
        - 用户管理
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - username
                - email
                - password
              properties:
                username:
                  type: string
                  description: 用户名,3-20个字符
                  minLength: 3
                  maxLength: 20
                  example: zhangsan
                email:
                  type: string
                  format: email
                  description: 用户邮箱
                  example: zhangsan@example.com
                password:
                  type: string
                  format: password
                  description: 密码,至少8位
                  minLength: 8
                  example: "Abc12345"
      responses:
        '201':
          description: 注册成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: integer
                    description: 业务状态码
                    example: 0
                  message:
                    type: string
                    description: 提示信息
                    example: 注册成功
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        description: 用户ID
                        example: 10001
                      username:
                        type: string
                        description: 用户名
                        example: zhangsan
                      email:
                        type: string
                        description: 邮箱
                        example: zhangsan@example.com
                      createdAt:
                        type: string
                        format: date-time
                        description: 注册时间
                        example: 2025-01-15T10:30:00Z
        '400':
          description: 请求参数错误
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: integer
                    description: 业务状态码
                    example: 1001
                  message:
                    type: string
                    description: 错误描述
                    example: 用户名已存在
                  data:
                    type: object
                    nullable: true
                    properties:
                      field:
                        type: string
                        description: 出错的字段
                        example: username
                      reason:
                        type: string
                        description: 具体原因
                        example: 该用户名已被注册

这个契约,你保存成 register.yaml,然后扔到Swagger Editor里,马上就能看到一个可交互的API文档。前端可以直接在页面上调试,后端可以照着它生成代码。

4.6 写契约的三个「避坑指南」

最后,分享几个我踩过的坑:

坑 后果 怎么避免
不写 required 前端以为字段可选,结果一传空值就报错 每个字段都问自己:这个能空吗?不能就加上 required
不写 example 前端不知道传什么,天天来问 每个字段都给一个合理的示例值
只写成功响应 出错了前端不知道怎么处理 至少把400、500的响应结构写清楚
字段类型用 string 糊弄 日期传成了时间戳,金额传成了字符串 能用 format 就用,比如 date-time、int32

嗯,这一章的内容就到这儿。你照着上面的契约,自己动手写一个试试。写完之后,用Swagger Editor打开看看效果。相信我,当你看到那个漂亮的交互式文档时,你会觉得——嗯,这活儿干得值。