JSON API Best Practices for Developers

Design better JSON APIs with consistent naming, structured responses, proper pagination, error handling, and performance optimizations used by top engineering teams.

JSONTech 团队February 1, 202510 min read

为什么 API 设计很重要

一个设计良好的 JSON API 是集成花费一个下午与花费一周之间的区别。良好的 API 设计减少支持工单,使文档更简洁,并让消费者在阅读文档之前就能预测事物的工作方式。

本指南中的模式不是理论性的——它们源自 Stripe、GitHub、Slack 和其他以出色开发者体验而闻名的公司的 API。让我们逐一了解这些模式。

命名约定

JSON 属性名称的两种主要风格是 camelCasesnake_case。只要选择一种并在所有地方使用,任意一种都是可以的。

约定示例使用者生态系统
camelCasefirstName, createdAtGoogle APIs, AzureJavaScript / TypeScript 前端
snake_casefirst_name, created_atStripe, GitHub, SlackPython / Ruby 后端

指导原则: 选择与您的主要消费者生态系统匹配的约定。如果您的 API 主要被 JavaScript 前端使用,camelCase 可以避免摩擦。如果您的后端是 Python 或 Ruby,并且 API 是 B2B,snake_case 是惯用的。

无论您选择什么,都要绝对一致。在同一响应中混合 user_namefirstName 会比几乎任何其他事情更快地侵蚀信任。

一致的响应结构

您的 API 的每个响应都应遵循可预测的封装。以下是适用于大多数 API 的模式:

成功响应

{
  "data": {
    "id": "usr_abc123",
    "email": "alice@example.com",
    "name": "Alice Johnson",
    "created_at": "2025-01-15T09:30:00Z"
  },
  "meta": {
    "request_id": "req_7f2a9c"
  }
}

列表响应

{
  "data": [
    { "id": "usr_abc123", "name": "Alice Johnson" },
    { "id": "usr_def456", "name": "Bob Smith" }
  ],
  "meta": {
    "total": 142,
    "page": 1,
    "per_page": 20,
    "request_id": "req_8b3d1e"
  }
}

错误响应

{
  "error": {
    "code": "validation_error",
    "message": "请求体包含无效字段。",
    "details": [
      {
        "field": "email",
        "message": "必须是有效的电子邮件地址。"
      },
      {
        "field": "age",
        "message": "必须至少为 13。"
      }
    ]
  },
  "meta": {
    "request_id": "req_4c8e2a"
  }
}

关键原则:成功时存在 data,失败时存在 errormeta 在两者中都出现以便于请求追踪和分页。消费者可以编写一个单一的响应处理程序,首先检查 error

自己试试: 将 API 响应粘贴到我们的 JSON 格式化工具 中,以检查结构并发现不一致之处。

分页模式

大多数列表端点需要分页。有三种常见的方法,每种方法都有不同的权衡:

1. 偏移分页

最简单的方法。客户端指定页码或偏移量:

GET /api/users?page=3&per_page=20

{
  "data": [...],
  "meta": {
    "total": 142,
    "page": 3,
    "per_page": 20,
    "total_pages": 8
  }
}

2. 游标分页

服务器返回一个不透明的游标,指向下一组结果,而不是页码:

GET /api/users?limit=20&after=cursor_abc123

{
  "data": [...],
  "meta": {
    "has_more": true,
    "next_cursor": "cursor_def456"
  }
}

3. 键集分页

使用真实的列值(如时间戳或 ID)作为游标。这是没有不透明性的游标分页:

GET /api/users?limit=20&created_after=2025-01-15T09:30:00Z

{
  "data": [...],
  "meta": {
    "has_more": true,
    "next_created_after": "2025-01-16T14:22:00Z"
  }
}

分页比较

方面偏移游标键集
跳转到第 N 页
总计数易于包含昂贵昂贵
大表上的性能降低(OFFSET 扫描行)一致一致
处理插入/删除可能跳过或重复项目稳定稳定
实现复杂性中等中等
使用者传统网页应用Stripe, SlackTwitter / X

指导原则: 对于需要用户跳转到特定页面的管理仪表板和内部工具,使用偏移分页。对于公共 API、无限滚动 UI 和任何频繁变化的数据集,使用游标或键集分页。

错误处理

良好的错误响应告诉消费者 发生了什么在哪里,并理想情况下 如何修复它。以下是我们推荐的结构:

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "您已超过每分钟 100 次请求的速率限制。",
    "details": [],
    "doc_url": "https://docs.example.com/errors/rate-limit"
  }
}

code 是机器可读的字符串(不是 HTTP 状态码——那在实际的 HTTP 响应中)。message 是人类可读的。details 数组提供验证失败的字段级错误。doc_url 链接到有关错误的文档。

常见错误代码及其 HTTP 状态映射:

HTTP 状态错误代码何时使用
400validation_error请求体未通过模式验证
401unauthorized缺少或无效的身份验证
403forbidden已认证但缺乏权限
404not_found资源不存在
409conflict重复资源或状态冲突
422unprocessable_entity有效 JSON 但语义上错误
429rate_limit_exceeded请求过多
500internal_error意外的服务器故障

数据类型和格式

正确的数据类型可以防止整个类别的集成错误:

日期和时间

始终使用 ISO 8601 格式并带有时区信息:

{
  "created_at": "2025-01-15T09:30:00Z",
  "expires_at": "2025-02-15T23:59:59+05:30"
}

绝不要在响应体中使用 Unix 时间戳——它们不具有人类可读性并且会造成时区模糊。如果您需要 Unix 时间戳(例如,用于 JWT 声明),请清楚地记录。

大数字

JavaScript 的 Number 类型在 2^53 - 1(9,007,199,254,740,991)之后会失去精度。如果您的 ID 或货币值超过此值,请使用字符串:

{
  "id": "9223372036854775807",
  "amount": "99.99",
  "currency": "USD"
}

Stripe、Twitter 和 Discord 都因这个原因使用字符串 ID。

Null 与缺失

定义明确的约定并记录:

  • null — 字段存在但没有值(例如,用户尚未设置个人简介)。
  • 缺失的键 — 字段与此资源无关或未被请求(例如,在部分响应中)。

许多 API 包括 null 字段以保持一致的形状,这使得客户端更容易编写类型接口,而不需要到处使用可选链。

布尔值

使用真实的 JSON 布尔值,而不是字符串或整数:

// 好
{ "is_active": true }

// 坏
{ "is_active": "true" }
{ "is_active": 1 }

版本控制策略

API 会发展。您需要一个策略来进行更改而不破坏现有消费者。三种主要方法:

  • URL 版本控制: /v1/users/v2/users。最常见和最明显的方法。由 Stripe、GitHub 和 Twilio 使用。易于理解,易于路由,但可能导致代码重复。
  • 头部版本控制: Accept: application/vnd.api+json;version=2。更干净的 URL,但不太易于发现。由 GitHub(作为替代方案)和 Azure 使用。
  • 查询参数: /users?version=2。简单但污染查询字符串。在实践中不太常见。

指导原则: URL 版本控制是务实的默认选项。它是显式的、可缓存的,并且适用于每个 HTTP 客户端。只有在您有特定技术原因时才偏离。

压缩

JSON 是文本,压缩效果极佳。始终为 API 响应启用压缩:

算法压缩比率速度浏览器支持
gzip良好(70–80% 减少)快速通用
Brotli (br)更好(75–85% 减少)压缩稍慢所有现代浏览器

对于典型的 50KB JSON 响应,gzip 将其压缩到约 10–15KB,而 Brotli 将其压缩到约 8–12KB。服务器应尊重 Accept-Encoding 头并相应选择。大多数反向代理(Nginx、Cloudflare)会自动处理此事。

性能提示

部分响应(字段选择)

让消费者只请求他们需要的字段。这减少了有效负载大小和数据库负载:

GET /api/users?fields=id,name,email

{
  "data": [
    { "id": "usr_abc123", "name": "Alice", "email": "alice@example.com" }
  ]
}

Google APIs 将此称为“部分响应”,并在所有端点上支持它。这对于在慢速连接上的移动客户端尤其有价值。

稀疏字段集

对于返回相关资源的 API,让消费者控制每种资源类型包含哪些字段:

GET /api/articles?include=author&fields[article]=title,body&fields[author]=name

这是来自 JSON:API 规范 的模式。它消除了客户端的 N+1 查询问题,同时保持有效负载最小。

ETags 和条件请求

在响应中返回 ETag 头。客户端可以在后续请求中发送 If-None-Match,如果没有更改,您返回 304 Not Modified(空体)。这为不经常更改的资源节省带宽和处理时间。

总结检查清单

在发布您的 API 之前,检查此清单:

  • 在所有端点中保持一致的命名约定(camelCasesnake_case)。
  • 可预测的响应封装,包含 dataerrormeta
  • 所有列表端点的分页以及文档化的限制。
  • 结构化的错误响应,包含机器可读的代码、消息和字段级详细信息。
  • ISO 8601 日期、大数字的字符串 ID、真实布尔值。
  • 文档化并强制执行的 API 版本控制策略。
  • 启用 gzip 或 Brotli 压缩。
  • 为性能敏感的消费者提供字段选择或稀疏字段集。

自己试试: 使用我们的 JSON 格式化工具 格式化和验证您的 API 响应,以确保它们遵循干净、一致的结构。

相关工具