跳到主要内容
API 设计的 7 个常见错误及最佳实践指南 | 极客日志
编程语言 Node.js 大前端
API 设计的 7 个常见错误及最佳实践指南 API 设计常见问题包括暴露数据库结构、HTTP 方法误用、错误处理混乱、URL 设计不规范、缺乏版本控制、返回数据冗余及查询参数不统一。通过对比错误案例与规范实现,结合 Python 与 Node.js 代码示例,阐述了 RESTful 风格、状态码使用、字段筛选、分页排序统一化等最佳实践,旨在帮助开发者构建易维护、高性能且对前端友好的接口。
云间漫步 发布于 2026/4/7 更新于 2026/7/8 33 浏览凌晨一点,前端在群里 @了你
'后端大哥,为什么删除用户的接口是 POST?'
'为什么获取用户列表要传 20 个参数?'
'为什么同一个错误,有时返回 200,有时返回 500?'
'能不能别再改接口了?这是这个月第三次了!'
你看着手机,心里一万头草泥马奔腾而过。
明明功能都实现了,为什么前端还是不满意?
因为你的 API 设计,可能犯了这 7 个致命错误。
今天,我们就来聊聊那些让前端抓狂、让自己背锅、让项目延期的 API 设计问题。
错误 1:把数据库表结构直接暴露成 API
问题示例
GET /api/user_account_info?user_id=123
{
"user_id" : 123 ,
"user_name" : "zhangsan" ,
"user_pwd_hash" : "5f4dcc3b5aa765d61d8327deb882cf99" ,
"user_create_ts" : 1704067200 ,
"user_last_login_ts" : 1736899200 ,
"user_status_flag" : 1 ,
"user_role_id" : 5 ,
"user_dept_id" : 10
}
问题在哪?
字段名丑陋 :user_pwd_hash、user_create_ts,这是给人看的还是给机器看的?
暴露实现细节 :前端不需要知道你用的是时间戳还是日期字符串
难以演进 :数据库改个字段名,API 就得跟着改,前端也得改
安全隐患 :密码哈希值为什么要返回?
规范示例
GET /api/users/123
{
"id" : 123 ,
"username" : "zhangsan" ,
"displayName" : "张三" ,
"createdAt" : "2024-01-01T00:00:00Z" ,
: ,
: ,
: { : , : }
}
"lastLoginAt"
"2025-01-15T10:30:00Z"
"status"
"active"
"role"
"id"
5
"name"
"editor"
核心原则:API 是给开发者用的,不是给数据库用的。
from rest_framework import serializers
class UserSerializer (serializers.ModelSerializer):
display_name = serializers.CharField(source='user_name' )
created_at = serializers.DateTimeField(source='user_create_ts' )
status = serializers.SerializerMethodField()
def get_status (self, obj ):
return 'active' if obj.user_status_flag == 1 else 'inactive'
class Meta :
model = User
fields = ['id' , 'username' , 'display_name' , 'created_at' , 'status' ]
exclude = ['user_pwd_hash' ]
app.get ("/api/users/:id" , async (req, res) => {
const user = await db.users .findById (req.params .id );
res.json ({
id : user.user_id ,
username : user.user_name ,
displayName : user.user_name ,
createdAt : new Date (user.user_create_ts * 1000 ).toISOString (),
status : user.user_status_flag === 1 ? "active" : "inactive"
});
});
错误 2:HTTP 方法乱用,POST 包打天下
问题示例
POST /api/getUser
POST /api/createUser
POST /api/updateUser
POST /api/deleteUser
POST /api/searchUsers
违反 HTTP 语义 :GET 请求应该是幂等的、可缓存的
无法利用浏览器缓存 :POST 请求不会被缓存
无法使用 CDN :CDN 通常只缓存 GET 请求
难以调试 :浏览器历史记录、书签都无法保存 POST 请求
规范示例:RESTful 风格
GET /api/users
GET /api/users/123
POST /api/users
PUT /api/users/123
PATCH /api/users/123
DELETE /api/users/123
方法 用途 幂等性 安全性 GET 获取资源 ✅ ✅ POST 创建资源 ❌ ❌ PUT 完整更新 ✅ ❌ PATCH 部分更新 ❌ ❌ DELETE 删除资源 ✅ ❌
幂等性 :多次执行结果相同
安全性 :不会修改服务器状态
实战代码
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/users' , methods=['GET' ] )
def get_users ():
page = request.args.get('page' , 1 , type =int )
limit = request.args.get('limit' , 20 , type =int )
users = User.query.paginate(page=page, per_page=limit)
return jsonify([user.to_dict() for user in users.items])
@app.route('/api/users/<int:user_id>' , methods=['GET' ] )
def get_user (user_id ):
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
@app.route('/api/users' , methods=['POST' ] )
def create_user ():
data = request.get_json()
user = User(username=data['username' ], email=data['email' ])
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
@app.route('/api/users/<int:user_id>' , methods=['PUT' ] )
def update_user (user_id ):
user = User.query.get_or_404(user_id)
data = request.get_json()
user.username = data['username' ]
user.email = data['email' ]
db.session.commit()
return jsonify(user.to_dict())
@app.route('/api/users/<int:user_id>' , methods=['PATCH' ] )
def patch_user (user_id ):
user = User.query.get_or_404(user_id)
data = request.get_json()
if 'username' in data: user.username = data['username' ]
if 'email' in data: user.email = data['email' ]
db.session.commit()
return jsonify(user.to_dict())
@app.route('/api/users/<int:user_id>' , methods=['DELETE' ] )
def delete_user (user_id ):
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return '' , 204
特殊场景:非 CRUD 操作怎么办? 有些业务操作不是简单的增删改查,比如发送邮件、重置密码、取消订单、点赞文章。
POST /api/sendEmail
POST /api/resetPassword
POST /api/cancelOrder
POST /api/emails
POST /api/password-resets
POST /api/orders/123 /cancellation
POST /api/posts/123 /likes
DELETE /api/posts/123 /likes
POST /api/users/123 /followers
DELETE /api/users/123 /followers
POST /api/users/123 /actions/activate
POST /api/orders/123 /actions/refund
POST /api/posts/123 /actions/publish
错误 3:错误处理一团糟
问题示例
{ "code" : 0 , "message" : "success" , "data" : { ...} }
{ "code" : 1001 , "message" : "用户不存在" , "data" : null }
{ "error" : "error" }
<!DOCTYPE html><html>...</html>
{ "error" : "用户名或密码错误" }
HTTP 状态码失去意义 :前端无法通过状态码判断请求是否成功
无法利用 HTTP 生态 :拦截器、中间件、监控工具都依赖状态码
错误信息不够详细 :前端不知道如何处理错误
难以调试 :出问题时无法快速定位
规范示例:标准化的错误响应
{
"error" : {
"code" : "INVALID_INPUT" ,
"message" : "Validation failed" ,
"details" : [
{ "field" : "email" , "message" : "Email format is invalid" } ,
{ "field" : "age" , "message" : "Age must be greater than 0" }
]
} ,
"requestId" : "req_abc123" ,
"timestamp" : "2025-01-17T10:30:00Z"
}
{
"error" : {
"code" : "UNAUTHORIZED" ,
"message" : "Authentication required" ,
"details" : "Please provide a valid access token"
}
}
{
"error" : {
"code" : "FORBIDDEN" ,
"message" : "Insufficient permissions" ,
"details" : "You need 'admin' role to perform this action"
}
}
{
"error" : {
"code" : "RESOURCE_NOT_FOUND" ,
"message" : "User not found" ,
"details" : "User with ID 123 does not exist"
}
}
{
"error" : {
"code" : "RATE_LIMIT_EXCEEDED" ,
"message" : "Too many requests" ,
"details" : "Rate limit: 100 requests per minute" ,
"retryAfter" : 45
}
}
{
"error" : {
"code" : "INTERNAL_SERVER_ERROR" ,
"message" : "An unexpected error occurred" ,
"details" : "Please contact support if the problem persists" ,
"requestId" : "req_abc123"
}
}
HTTP 状态码速查表
2xx 成功 :200 OK, 201 Created, 202 Accepted, 204 No Content, 206 Partial Content
4xx 客户端错误 :400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 405 Method Not Allowed, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests
5xx 服务器错误 :500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
实战代码:统一错误处理
from flask import Flask, jsonify
from datetime import datetime
import uuid
app = Flask(__name__)
class APIError (Exception ):
def __init__ (self, code, message, details=None , status_code=400 ):
self .code = code
self .message = message
self .details = details
self .status_code = status_code
@app.errorhandler(APIError )
def handle_api_error (error ):
response = {
'error' : {
'code' : error.code,
'message' : error.message,
'details' : error.details
},
'requestId' : str (uuid.uuid4()),
'timestamp' : datetime.utcnow().isoformat() + 'Z'
}
return jsonify(response), error.status_code
@app.errorhandler(404 )
def handle_not_found (error ):
return jsonify({'error' : {'code' : 'RESOURCE_NOT_FOUND' , 'message' : 'The requested resource was not found' }}), 404
@app.errorhandler(500 )
def handle_internal_error (error ):
app.logger.error(f'Internal error: {error} ' )
return jsonify({
'error' : {
'code' : 'INTERNAL_SERVER_ERROR' ,
'message' : 'An unexpected error occurred' ,
'details' : 'Please contact support if the problem persists'
},
'requestId' : str (uuid.uuid4())
}), 500
@app.route('/api/users/<int:user_id>' , methods=['GET' ] )
def get_user (user_id ):
user = User.query.get(user_id)
if not user:
raise APIError(
code='USER_NOT_FOUND' ,
message='User not found' ,
details=f'User with ID {user_id} does not exist' ,
status_code=404
)
return jsonify(user.to_dict())
const express = require ("express" );
const { v4 : uuidv4 } = require ("uuid" );
const app = express ();
class APIError extends Error {
constructor (code, message, details, statusCode = 400 ) {
super (message);
this .code = code;
this .details = details;
this .statusCode = statusCode;
}
}
app.use ((err, req, res, next ) => {
console .error ("Error:" , err);
if (err instanceof APIError ) {
return res.status (err.statusCode ).json ({
error : {
code : err.code ,
message : err.message ,
details : err.details
},
requestId : uuidv4 (),
timestamp : new Date ().toISOString ()
});
}
res.status (500 ).json ({
error : {
code : "INTERNAL_SERVER_ERROR" ,
message : "An unexpected error occurred" ,
details : process.env .NODE_ENV === "development" ? err.message : undefined
},
requestId : uuidv4 (),
timestamp : new Date ().toISOString ()
});
});
app.get ("/api/users/:id" , async (req, res, next) => {
try {
const user = await User .findById (req.params .id );
if (!user) {
throw new APIError ("USER_NOT_FOUND" , "User not found" , `User with ID ${req.params.id} does not exist` , 404 );
}
res.json (user);
} catch (error) {
next (error);
}
});
错误 4:URL 设计混乱不堪
问题示例
GET /api/getUsers
GET /api/user/list
GET /api/fetchUserData
GET /api/v1/company/123 /department/456 /team/789 /user/111 /posts/222 /comments/333
GET /api/users
GET /api/product
GET /api/order-list
GET /api/userProfile
GET /api/users/search/name/zhangsan/age/25
规范示例:清晰的 URL 设计
GET /api/users
GET /api/getUsers
GET /api/users
GET /api/user
GET /api/user-profiles
GET /api/userProfiles
GET /api/user_profiles
GET /api/users/123 /posts
GET /api/posts?userId=123
GET /api/users/123 /posts/456 /comments
GET /api/users?name=zhangsan&age=25
GET /api/users/search/name/zhangsan
GET /api/users?page=1 &limit=20
GET /api/users/page/1 /limit/20
URL 设计速查表 场景 推荐方式 说明 获取列表 GET /api/users使用复数名词 获取单个 GET /api/users/123ID 放在路径中 搜索过滤 GET /api/users?name=zhang&age=25使用查询参数 分页 GET /api/users?page=1&limit=20使用查询参数 排序 GET /api/users?sort=createdAt&order=desc使用查询参数 关联资源 GET /api/users/123/posts嵌套不超过 2 层 字段筛选 GET /api/users?fields=id,name,email使用查询参数 版本控制 GET /api/v1/users版本号放在路径开头
实战示例:完整的用户 API
GET /api/v1/users
GET /api/v1/users/123
POST /api/v1/users
PUT /api/v1/users/123
PATCH /api/v1/users/123
DELETE /api/v1/users/123
GET /api/v1/users/123 /posts
POST /api/v1/users/123 /posts
GET /api/v1/users/123 /followers
POST /api/v1/users/123 /followers
DELETE /api/v1/users/123 /followers/456
GET /api/v1/users?name=zhang
GET /api/v1/users?age=25 &city=beijing
GET /api/v1/users?status=active
GET /api/v1/users?page=1 &limit=20
GET /api/v1/users?sort=createdAt&order=desc
GET /api/v1/users?fields=id,name,email
错误 5:没有版本控制,改接口全靠吼
问题示例
GET /api/users/123
{ "name" : "张三" , "age" : 25 }
GET /api/users/123
{ "firstName" : "三" , "lastName" : "张" , "birthDate" : "1999-01-01" }
向后兼容 :老版本的客户端不会因为 API 更新而崩溃
平滑迁移 :给客户端足够的时间升级
A/B 测试 :可以同时运行多个版本
回滚方便 :出问题可以快速回退
规范示例:API 版本控制
GET /api/v1/users/123
GET /api/v2/users/123
GET /api/users/123
Accept : application/vnd.myapi .v1 +json
GET /api/users/123
Accept : application/vnd.myapi .v2 +json
GET /api/users/123 ?version=1
GET /api/users/123 ?version=2
实战代码:版本控制实现
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.route('/api/v1/users/<int:user_id>' )
def get_user_v1 (user_id ):
user = User.query.get_or_404(user_id)
return jsonify({'name' : user.name, 'age' : user.age})
@app.route('/api/v2/users/<int:user_id>' )
def get_user_v2 (user_id ):
user = User.query.get_or_404(user_id)
return jsonify({
'firstName' : user.first_name,
'lastName' : user.last_name,
'birthDate' : user.birth_date.isoformat()
})
@app.route('/api/users/<int:user_id>' )
def get_user (user_id ):
version = request.headers.get('API-Version' , 'v1' )
user = User.query.get_or_404(user_id)
if version == 'v1' :
return jsonify({'name' : user.name, 'age' : user.age})
elif version == 'v2' :
return jsonify({
'firstName' : user.first_name,
'lastName' : user.last_name,
'birthDate' : user.birth_date.isoformat()
})
else :
return jsonify({'error' : 'Unsupported API version' }), 400
const express = require ("express" );
const app = express ();
const v1Router = express.Router ();
const v2Router = express.Router ();
v1Router.get ("/users/:id" , async (req, res) => {
const user = await User .findById (req.params .id );
res.json ({ name : user.name , age : user.age });
});
v2Router.get ("/users/:id" , async (req, res) => {
const user = await User .findById (req.params .id );
res.json ({ firstName : user.firstName , lastName : user.lastName , birthDate : user.birthDate });
});
app.use ("/api/v1" , v1Router);
app.use ("/api/v2" , v2Router);
app.use ("/api" , v2Router);
版本废弃策略
app.use ('/api/v1' , (req, res, next ) => {
res.set ({
'X-API-Deprecated' : 'true' ,
'X-API-Deprecation-Date' : '2025-12-31' ,
'X-API-Sunset-Date' : '2026-03-31' ,
'Link' : '</api/v2>; rel="successor-version"'
});
next ();
});
{
"data" : {...},
"meta" : {
"deprecated" : true ,
"deprecationDate" : "2025-12-31" ,
"sunsetDate" : "2026-03-31" ,
"message" : "This API version will be sunset on 2026-03-31. Please migrate to v2." ,
"migrationGuide" : "https://docs.example.com/api/v1-to-v2-migration"
}
}
错误 6:返回数据一股脑全给,前端要啥给啥
问题示例
GET /api/users
[
{
"id" : 1 ,
"username" : "zhangsan" ,
"email" : "[email protected] " ,
"passwordHash" : "5f4dcc3b5aa765d61d8327deb882cf99" ,
"phoneNumber" : "13800138000" ,
"address" : "北京市朝阳区..." ,
"bio" : "这是一段很长的个人简介..." ,
"settings" : { "theme" : "dark" , "language" : "zh-CN" , "notifications" : { ...} } ,
"posts" : [ ...] ,
"followers" : [ ...] ,
"following" : [ ...] ,
"createdAt" : "2024-01-01T00:00:00Z" ,
"updatedAt" : "2025-01-15T10:30:00Z" ,
"lastLoginAt" : "2025-01-17T08:00:00Z" ,
"loginCount" : 1234 ,
"ipAddress" : "192.168.1.1"
}
]
规范示例:按需返回数据 方案 1:字段筛选(Sparse Fieldsets)
GET /api/users?fields=id,username,email
[
{"id" : 1 , "username" : "zhangsan" , "email" : "[email protected] " },
{"id" : 2 , "username" : "lisi" , "email" : "[email protected] " }
]
GET /api/users?fields=id,username,profile.avatar ,profile.bio
[
{"id" : 1 , "username" : "zhangsan" , "profile" : {"avatar" : "https://..." , "bio" : "..." }}
]
GET /api/users
[{
"id" : 1 ,
"username" : "zhangsan" ,
"avatar" : "https://..." ,
"bio" : "简短的个人简介..."
}]
GET /api/users/1
{
"id" : 1 ,
"username" : "zhangsan" ,
"email" : "[email protected] " ,
"avatar" : "https://..." ,
"bio" : "完整的个人简介..." ,
"profile" : {"location" : "北京" , "website" : "https://..." , "joinedAt" : "2024-01-01T00:00:00Z" },
"stats" : {"postsCount" : 42 , "followersCount" : 1234 , "followingCount" : 567 }
}
query { users { id username email } }
query { users { id username email profile { avatar bio } posts( limit : 5 ) { id title } } }
实战代码:字段筛选实现
from flask import Flask, request, jsonify
app = Flask(__name__)
def filter_fields (data, fields ):
if not fields: return data
field_list = fields.split(',' )
if isinstance (data, list ):
return [filter_fields(item, fields) for item in data]
if isinstance (data, dict ):
filtered = {}
for field in field_list:
if '.' in field:
parts = field.split('.' , 1 )
parent, child = parts[0 ], parts[1 ]
if parent in data:
if parent not in filtered: filtered[parent] = {}
filtered[parent].update(filter_fields(data[parent], child))
elif field in data:
filtered[field] = data[field]
return filtered
return data
@app.route('/api/users' )
def get_users ():
users = User.query.all ()
data = [user.to_dict() for user in users]
fields = request.args.get('fields' )
if fields:
data = filter_fields(data, fields)
return jsonify(data)
const express = require ("express" );
const app = express ();
function fieldFilter (req, res, next ) {
const originalJson = res.json .bind (res);
res.json = function (data ) {
const fields = req.query .fields ;
if (fields) {
const fieldList = fields.split ("," );
data = filterFields (data, fieldList);
}
return originalJson (data);
};
next ();
}
function filterFields (data, fields ) {
if (Array .isArray (data)) {
return data.map ((item ) => filterFields (item, fields));
}
if (typeof data === "object" && data !== null ) {
const filtered = {};
fields.forEach ((field ) => {
if (field.includes ("." )) {
const [parent, ...rest] = field.split ("." );
if (data[parent]) {
filtered[parent] = filtered[parent] || {};
Object .assign (filtered[parent], filterFields (data[parent], [rest.join ("." )]));
}
} else if (field in data) {
filtered[field] = data[field];
}
});
return filtered;
}
return data;
}
app.use (fieldFilter);
app.get ("/api/users" , async (req, res) => {
const users = await User .findAll ();
res.json (users);
});
错误 7:分页、排序、过滤各自为政
问题示例
GET /api/users?page=1 &pageSize=20
GET /api/posts?pageNum=1 &limit=20
GET /api/comments?p=1 &size=20 &offset=0
GET /api/orders?start=0 &count=20
降低学习成本 :前端不用记住每个接口的参数名
便于封装 :可以写一个通用的分页组件
减少错误 :统一的规范减少参数错误
提升体验 :一致性让 API 更专业
规范示例:统一的查询规范
GET /api/users?page=1 &limit=20
GET /api/users?sort=createdAt&order=desc
GET /api/users?sort=-createdAt
GET /api/users?status=active&role=admin
GET /api/users?age[gte]=18 &age[lte]=60
GET /api/users?search=zhang
GET /api/users?page=1 &limit=20 &sort=-createdAt&status=active&search=zhang
标准响应格式
{
"data" : [ { "id" : 1 , "username" : "zhangsan" } , { "id" : 2 , "username" : "lisi" } ] ,
"pagination" : {
"page" : 1 ,
"limit" : 20 ,
"total" : 100 ,
"totalPages" : 5 ,
"hasNext" : true ,
"hasPrev" : false
} ,
"links" : {
"self" : "/api/users?page=1&limit=20" ,
"first" : "/api/users?page=1&limit=20" ,
"last" : "/api/users?page=5&limit=20" ,
"next" : "/api/users?page=2&limit=20" ,
"prev" : null
}
}
实战代码:通用分页实现
from flask import Flask, request, jsonify, url_for
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app)
def paginate (query, page=1 , limit=20 ):
total = query.count()
total_pages = (total + limit - 1 ) // limit
items = query.offset((page - 1 ) * limit).limit(limit).all ()
return {
'data' : [item.to_dict() for item in items],
'pagination' : {
'page' : page,
'limit' : limit,
'total' : total,
'totalPages' : total_pages,
'hasNext' : page < total_pages,
'hasPrev' : page > 1
}
}
@app.route('/api/users' )
def get_users ():
page = request.args.get('page' , 1 , type =int )
limit = request.args.get('limit' , 20 , type =int )
sort = request.args.get('sort' , 'id' )
order = request.args.get('order' , 'asc' )
search = request.args.get('search' , '' )
status = request.args.get('status' , '' )
query = User.query
if search:
query = query.filter (User.username.like(f'%{search} %' ))
if status:
query = query.filter (User.status == status)
if order == 'desc' :
query = query.order_by(getattr (User, sort).desc())
else :
query = query.order_by(getattr (User, sort).asc())
result = paginate(query, page, limit)
return jsonify(result)
const express = require ("express" );
const { Op } = require ("sequelize" );
const app = express ();
async function paginate (model, options = {} ) {
const { page = 1 , limit = 20 , where = {}, order = [["id" , "ASC" ]], attributes } = options;
const offset = (page - 1 ) * limit;
const { count, rows } = await model.findAndCountAll ({ where, order, limit, offset, attributes });
const totalPages = Math .ceil (count / limit);
return {
data : rows,
pagination : {
page,
limit,
total : count,
totalPages,
hasNext : page < totalPages,
hasPrev : page > 1
}
};
}
app.get ("/api/users" , async (req, res) => {
try {
const page = parseInt (req.query .page ) || 1 ;
const limit = parseInt (req.query .limit ) || 20 ;
const search = req.query .search || "" ;
const status = req.query .status || "" ;
const sort = req.query .sort || "id" ;
const order = req.query .order || "asc" ;
const where = {};
if (search) where.username = { [Op .like ]: `%${search} %` };
if (status) where.status = status;
const orderBy = [[sort, order.toUpperCase ()]];
const result = await paginate (User , { page, limit, where, order : orderBy });
res.json (result);
} catch (error) {
res.status (500 ).json ({ error : error.message });
}
});
高级过滤语法
GET /api/users?status=active
GET /api/users?status[ne]=inactive
GET /api/users?age[gt]=18 &age[lt]=60
GET /api/users?age[gte]=18 &age[lte]=60
GET /api/users?role[in ]=admin,editor
GET /api/users?role[nin]=guest
GET /api/users?username[like]=zhang
GET /api/users?createdAt[gte]=2024 -01 -01 &createdAt[lte]=2024 -12 -31
GET /api/users?status=active&age[gte]=18 &role[in ]=admin,editor&sort=-createdAt
总结:API 设计的黄金法则
以开发者为中心 :API 是给开发者用的,不是给数据库用的。设计时要站在使用者的角度思考。
保持一致性 :命名规范、错误格式、分页参数…所有接口都应该遵循相同的规范。
使用标准 :HTTP 方法、状态码、日期格式…尽量使用业界标准,不要自己发明。
文档先行 :好的文档胜过千言万语。使用 OpenAPI/Swagger 自动生成文档。
版本控制 :从第一天就考虑版本控制,不要等到需要改接口时才想起来。
性能优化 :分页、字段筛选、缓存…从设计阶段就考虑性能问题。
安全第一 :认证、授权、限流、数据脱敏…安全永远是第一位的。
API 设计检查清单
URL 使用名词而不是动词?
使用了正确的 HTTP 方法?
命名规范统一(全部用短横线或驼峰)?
资源嵌套不超过 2 层?
使用 JSON 作为默认格式?
日期使用 ISO 8601 格式?
字段名使用驼峰命名?
没有暴露数据库实现细节?
使用了正确的 HTTP 状态码?
错误响应包含 code、message、details?
提供了 requestId 用于追踪?
错误信息对开发者友好?
有明确的版本号(v1、v2)?
有版本废弃策略?
文档中说明了版本差异?
支持分页?
支持字段筛选?
支持排序和过滤?
考虑了缓存策略?
使用 HTTPS?
有认证机制?
有权限控制?
有限流保护?
敏感数据已脱敏?
有完整的 API 文档?
有请求/响应示例?
有错误码说明?
有变更日志?
推荐工具
API 设计工具
Swagger/OpenAPI
自动生成 API 文档
支持在线测试
可以生成客户端代码
openapi: 3.0 .0
info:
title: User API
version: 1.0 .0
paths:
/api/v1/users:
get:
summary: 获取用户列表
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
"200":
description: 成功
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: $ref: "#/components/schemas/User"
pagination: $ref: "#/components/schemas/Pagination"
Postman
Insomnia
轻量级 API 测试工具
支持 GraphQL
界面简洁
API 网关
Kong
Nginx
AWS API Gateway
云原生 API 网关
自动扩展
与 AWS 服务集成
监控工具
写在最后:好的 API 是产品的一部分
让前端开发效率提升 50%
让 Bug 数量减少 70%
让新人上手时间缩短 80%
让产品迭代速度加快 2 倍
让前端天天骂娘
让 Bug 层出不穷
让新人一脸懵逼
让产品迭代举步维艰
❌ 暴露数据库结构
❌ HTTP 方法乱用
❌ 错误处理混乱
❌ URL 设计混乱
❌ 没有版本控制
❌ 返回数据过多
❌ 查询参数不统一
彩蛋:一个完整的 API 设计示例
GET /api/v1/users?page=1 &limit=20 &search=zhang&status=active&sort=-createdAt
Response : 200 OK
{
"data" : [...],
"pagination" : {
"page" : 1 ,
"limit" : 20 ,
"total" : 100 ,
"totalPages" : 5 ,
"hasNext" : true ,
"hasPrev" : false
}
}
GET /api/v1/users/123
Response : 200 OK
{
"id" : 123 ,
"username" : "zhangsan" ,
"email" : "[email protected] " ,
"profile" : {...},
"createdAt" : "2024-01-01T00:00:00Z"
}
POST /api/v1/users
Request Body : {"username" : "zhangsan" , "email" : "[email protected] " , "password" : "********" }
Response : 201 Created
{
"id" : 123 ,
"username" : "zhangsan" ,
"email" : "[email protected] " ,
"createdAt" : "2025-01-17T10:30:00Z"
}
PATCH /api/v1/users/123
Request Body : {"email" : "[email protected] " }
Response : 200 OK
{
"id" : 123 ,
"username" : "zhangsan" ,
"email" : "[email protected] " ,
"updatedAt" : "2025-01-17T10:35:00Z"
}
DELETE /api/v1/users/123
Response : 204 No Content
GET /api/v1/users/123 /posts?page=1 &limit=10
GET /api/v1/users/123 /followers
POST /api/v1/users/123 /followers
Request Body : {"userId" : 456 }
DELETE /api/v1/users/123 /followers/456
{
"error" : {
"code" : "VALIDATION_ERROR" ,
"message" : "Validation failed" ,
"details" : [{"field" : "email" , "message" : "Email format is invalid" }]
},
"requestId" : "req_abc123" ,
"timestamp" : "2025-01-17T10:30:00Z"
}
{
"error" : {
"code" : "USER_NOT_FOUND" ,
"message" : "User not found" ,
"details" : "User with ID 123 does not exist"
},
"requestId" : "req_abc123" ,
"timestamp" : "2025-01-17T10:30:00Z"
}
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online