定义你的第一个API契约:路径、方法、请求参数、响应体
好,咱们现在开始动手。前面几章聊了数据契约的理念和价值,说实话,光说不练容易飘。这一章,我带你亲手写一个真正的API契约。
你可能会问:「不就是写个接口文档吗?用Word画个表格不就行了?」嗯,我以前也这么干过。结果呢?文档和代码对不上,前端说字段名错了,后端说文档没更新,最后两边一起找我吵架。从那以后,我学乖了——契约必须机器可读、可验证、可执行。
今天我们用OpenAPI 3.0规范,定义一个用户注册接口。别怕,这东西没那么玄乎。
4.1 先搞清楚:契约里到底要写什么?
一个API契约,说白了就是你和调用方之间的「合同」。合同里得写清楚:
- 路径:这个接口长什么样?比如
/api/v1/users/register - 方法:是查数据(GET)、创建数据(POST)、还是改数据(PUT)?
- 请求参数:调用方需要带什么东西过来?是URL里的参数,还是请求体里的JSON?
- 响应体:你返回什么给人家?成功长什么样,失败长什么样?
我个人的习惯是,先画一张图,把这几块的关系理清楚。你看下面这张图:
这张图把一次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给个示例值。这招特别好用——前端开发拿到契约,直接复制示例就能调接口,省得来回问。
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打开看看效果。相信我,当你看到那个漂亮的交互式文档时,你会觉得——嗯,这活儿干得值。