引言
上周帮一个朋友Code Review代码,他写了一个用户管理接口:
plaintext
GET /getUserById?id=1
POST /createUser
POST /updateUser
GET /deleteUser?id=1
我看完差点没背过气去。这接口设计…怎么说呢,能用,但总觉得哪里不对劲。
后来我问他为什么这么设计,他说:”我看很多老项目都是这么写的啊!”
好吧,这篇文章就是来解决这个问题的。我会把我这些年设计API踩过的坑、总结的经验全部写下来,希望能帮你设计出更规范的接口。

REST是什么
在说RESTful API之前,先得搞清楚REST是什么。
REST(Representational State Transfer)是Roy Fielding在2000年提出的架构风格,不是标准,也不是协议。它描述了一种设计Web服务的思路。
简单说,REST的核心思想是:用URL定位资源,用HTTP动词描述操作。
比如:
| 操作 | RESTful写法 | 非RESTful写法 |
|---|---|---|
| 获取用户列表 | GET /users | GET /getUsers |
| 获取单个用户 | GET /users/1 | GET /getUserById?id=1 |
| 创建用户 | POST /users | POST /createUser |
| 更新用户 | PUT /users/1 | POST /updateUser |
| 删除用户 | DELETE /users/1 | GET /deleteUser?id=1 |
看到了吗?RESTful的核心就是把所有的”动作”都用HTTP方法来表示,而不是在URL里写动词。
URL设计规范
URL(Uniform Resource Locator)是API的门面,设计得好不好直接影响开发者体验。
基本原则
- 用名词表示资源,不用动词
bash
# 好
GET /users
GET /users/123
GET /articles/456/comments
# 不好
GET /getUsers
GET /getUser
GET /queryUsersById
- 用复数名词表示集合
bash
# 好
GET /users
POST /users
# 不好
GET /user
POST /userList
- 用小写字母,用连字符分隔单词
bash
# 好
GET /user-profiles
GET /blog-posts
# 不推荐(虽然也能工作)
GET /userProfiles
GET /blog_posts # 下划线在某些字体里容易看不清
- 层级关系用斜杠表示
bash
# 获取用户123的所有文章
GET /users/123/articles
# 获取用户123的文章456的评论
GET /users/123/articles/456/comments
查询参数的使用
当需要对资源进行过滤、排序、分页时,使用查询参数:
bash
# 过滤
GET /users?status=active&role=admin
# 排序
GET /users?sort=created_at&order=desc
# 分页
GET /users?page=2&page_size=20
# 搜索
GET /users?search=张三
避免过深的层级
虽然REST支持多层嵌套,但不要太深:
bash
# 不推荐(嵌套太深)
GET /organizations/123/departments/456/teams/789/members/101
# 推荐(扁平化设计)
GET /members/101?organization=123&department=456&team=789
HTTP方法:正确使用动词
HTTP定义了一组方法(也叫动词),RESTful API要用这些方法来表示操作。
常用方法
| 方法 | 用途 | 幂等性 | 安全性 |
|---|---|---|---|
| GET | 获取资源 | 是 | 是 |
| POST | 创建资源 | 否 | 否 |
| PUT | 完整更新资源 | 是 | 否 |
| PATCH | 部分更新资源 | 否 | 否 |
| DELETE | 删除资源 | 是 | 否 |
幂等性:同样的请求执行一次和执行多次,效果是一样的。
安全性:不会改变服务器状态。
GET:获取资源
bash
# 获取所有用户(列表)
GET /users
# 获取单个用户
GET /users/123
# 获取用户的好友列表
GET /users/123/friends
# 获取多个特定用户
GET /users?id=1&id=2&id=3
POST:创建资源
bash
# 创建用户
POST /users
Content-Type: application/json
{
"name": "张三",
"email": "zhangsan@example.com",
"password": "123456"
}
响应:
http
HTTP/1.1 201 Created
Location: /users/456
Content-Type: application/json
{
"id": 456,
"name": "张三",
"email": "zhangsan@example.com",
"created_at": "2026-04-15T10:30:00Z"
}
注意返回状态码201和Location头。
PUT:完整更新资源
PUT要求提交完整的资源数据,缺少的字段会被设为默认值或清空:
bash
PUT /users/456
Content-Type: application/json
{
"name": "张三改",
"email": "zhangsan_changed@example.com",
"password": "654321",
"status": "active"
}
PATCH:部分更新资源
PATCH只更新提供的字段,其他字段保持不变:
bash
PATCH /users/456
Content-Type: application/json
{
"email": "zhangsan_new@example.com"
}
DELETE:删除资源
bash
DELETE /users/456
成功删除返回204 No Content:
http
HTTP/1.1 204 No Content
状态码:让客户端知道发生了什么
状态码是HTTP响应的核心部分,它告诉客户端请求的结果是什么。
常用状态码
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 OK | 成功 | GET、PUT、PATCH成功 |
| 201 Created | 创建成功 | POST创建新资源成功 |
| 204 No Content | 无内容 | DELETE成功,响应无body |
| 400 Bad Request | 请求错误 | 参数校验失败、格式错误 |
| 401 Unauthorized | 未认证 | 需要登录 |
| 403 Forbidden | 无权限 | 已登录但无权限 |
| 404 Not Found | 资源不存在 | 找不到指定资源 |
| 409 Conflict | 冲突 | 资源冲突,如重复创建 |
| 422 Unprocessable Entity | 验证失败 | 数据验证失败 |
| 500 Internal Server Error | 服务器错误 | 程序异常 |
| 503 Service Unavailable | 服务不可用 | 维护或过载 |
分层状态码
不要只用200和500,要根据情况返回合适的状态码:
javascript
// Express.js 示例
app.get('/users/:id', async (req, res) => {
try {
const userId = parseInt(req.params.id);
if (isNaN(userId)) {
return res.status(400).json({
error: 'Invalid user ID',
message: 'User ID must be a number'
});
}
const user = await db.getUser(userId);
if (!user) {
return res.status(404).json({
error: 'User not found',
message: `No user with ID ${userId}`
});
}
res.json(user);
} catch (error) {
console.error('Database error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Please try again later'
});
}
});
请求和响应格式
JSON是你的好朋友
现代API基本都用JSON作为数据格式:
http
POST /users
Content-Type: application/json
Accept: application/json
{
"name": "张三",
"email": "zhangsan@example.com"
}
响应:
http
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": 123,
"name": "张三",
"email": "zhangsan@example.com",
"created_at": "2026-04-15T10:30:00Z"
}
统一的响应结构
建议所有API都用统一的响应格式:
javascript
// 成功响应
{
"success": true,
"data": { ... },
"message": "操作成功"
}
// 分页响应
{
"success": true,
"data": {
"items": [...],
"pagination": {
"page": 1,
"page_size": 20,
"total": 100,
"total_pages": 5
}
}
}
// 错误响应
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "参数校验失败",
"details": [
{ "field": "email", "message": "邮箱格式不正确" },
{ "field": "password", "message": "密码长度不能少于6位" }
]
}
}
时间格式
统一使用ISO 8601格式:
json
{
"created_at": "2026-04-15T10:30:00Z",
"updated_at": "2026-04-15T12:45:30+08:00"
}
字段命名
统一用驼峰或蛇形,两者选其一:
json
// 驼峰(推荐,JavaScript风格)
{
"userId": 123,
"userName": "张三",
"createdAt": "2026-04-15T10:30:00Z"
}
// 蛇形(Python风格)
{
"user_id": 123,
"user_name": "张三",
"created_at": "2026-04-15T10:30:00Z"
}
认证与授权
认证(Authentication)
确认”你是谁”,常用方式:
1. API Key
bash
GET /users?api_key=your_api_key_here
适合服务端之间的调用,不适合用户认证。
2. Bearer Token(JWT)
bash
GET /users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
最常用的方式,适合移动端和前端。
3. OAuth 2.0
适合第三方登录,如微信登录、Google登录等。
授权(Authorization)
确认”你能做什么”,常用方式:
javascript
// 简单角色检查
function authorize(roles = []) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// 使用中间件
app.delete('/users/:id',
authenticate, // 确认登录
authorize(['admin']), // 确认是管理员
async (req, res) => {
// 删除用户逻辑
}
);
分页、排序和过滤
分页
bash
# 页码分页
GET /users?page=2&page_size=20
# 偏移分页(适合大数据量)
GET /users?offset=40&limit=20
# 光标分页(适合实时数据)
GET /users?cursor=abc123&limit=20
响应示例:
json
{
"data": [...],
"pagination": {
"page": 2,
"page_size": 20,
"total": 100,
"total_pages": 5,
"has_next": true,
"has_prev": true
}
}
排序
bash
# 按创建时间降序
GET /users?sort=created_at&order=desc
# 多字段排序
GET /users?sort=role,created_at&order=asc,desc
过滤
bash
# 单个条件
GET /users?status=active
# 多个条件
GET /users?status=active&role=admin
# 范围查询
GET /users?age_gte=18&age_lte=60
# 模糊搜索
GET /users?search=张三
错误处理
错误响应格式
json
{
"success": false,
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "请求的资源不存在",
"details": {
"resource": "user",
"id": "12345"
}
},
"request_id": "req_abc123"
}
错误码设计
建议使用枚举定义错误码:
javascript
const ErrorCodes = {
// 通用错误(1000-1999)
INTERNAL_ERROR: { code: 1000, status: 500, message: '服务器内部错误' },
INVALID_PARAMETER: { code: 1001, status: 400, message: '参数错误' },
// 认证错误(2000-2999)
UNAUTHORIZED: { code: 2000, status: 401, message: '未认证' },
TOKEN_EXPIRED: { code: 2001, status: 401, message: 'Token已过期' },
// 权限错误(3000-3999)
FORBIDDEN: { code: 3000, status: 403, message: '无权限访问' },
// 资源错误(4000-4999)
NOT_FOUND: { code: 4000, status: 404, message: '资源不存在' },
ALREADY_EXISTS: { code: 4001, status: 409, message: '资源已存在' },
// 业务错误(5000-5999)
INSUFFICIENT_BALANCE: { code: 5000, status: 400, message: '余额不足' }
};
// 统一错误处理函数
function handleError(errorCode, details = {}) {
return {
success: false,
error: {
code: errorCode.code,
message: errorCode.message,
...details
}
};
}
// 使用
app.get('/users/:id', async (req, res) => {
const user = await getUser(req.params.id);
if (!user) {
return res.status(404).json(handleError(ErrorCodes.NOT_FOUND, { resource: 'user' }));
}
res.json(user);
});
API版本管理
当API需要升级但不兼容旧版本时,需要版本管理。
URL路径版本(推荐)
bash
GET /v1/users
GET /v2/users
优点:直观,易于调试
缺点:代码需要维护多个版本
Header版本
bash
GET /users
Accept: application/vnd.example.v2+json
优点:URL保持干净
缺点:调试不方便
版本共存策略
javascript
// Express.js 示例
const v1Routes = require('./routes/v1');
const v2Routes = require('./routes/v2');
app.use('/v1', v1Routes);
app.use('/v2', v2Routes);
废弃旧版本
http
GET /v1/users
# 响应头提示
Deprecation: true
Sunset: Thu, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
实战:完整的用户管理API
项目结构
plaintext
user-api/
├── src/
│ ├── routes/
│ │ └── users.js
│ ├── middleware/
│ │ ├── auth.js
│ │ └── errorHandler.js
│ ├── models/
│ │ └── User.js
│ ├── controllers/
│ │ └── userController.js
│ ├── utils/
│ │ └── errors.js
│ └── app.js
├── package.json
└── README.md
Express.js实现
src/utils/errors.js:错误定义
javascript
class AppError extends Error {
constructor(code, statusCode, message) {
super(message);
this.code = code;
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, details = []) {
super('VALIDATION_ERROR', 400, message);
this.details = details;
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super('NOT_FOUND', 404, `${resource} with id ${id} not found`);
this.resource = resource;
this.resourceId = id;
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super('UNAUTHORIZED', 401, message);
}
}
class ForbiddenError extends AppError {
constructor(message = 'Access denied') {
super('FORBIDDEN', 403, message);
}
}
module.exports = {
AppError,
ValidationError,
NotFoundError,
UnauthorizedError,
ForbiddenError
};
src/middleware/errorHandler.js:错误处理中间件
javascript
const { AppError } = require('../utils/errors');
function errorHandler(err, req, res, next) {
console.error('Error:', err);
if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
error: {
code: err.code,
message: err.message,
details: err.details || undefined
},
request_id: req.id
});
}
// 开发环境返回详细错误
if (process.env.NODE_ENV === 'development') {
return res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: err.message,
stack: err.stack
}
});
}
// 生产环境返回通用错误
res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred'
}
});
}
module.exports = errorHandler;
src/middleware/auth.js:认证中间件
javascript
const jwt = require('jsonwebtoken');
const { UnauthorizedError } = require('../utils/errors');
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedError('No token provided');
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new UnauthorizedError('Token expired');
}
throw new UnauthorizedError('Invalid token');
}
}
function authorize(...roles) {
return (req, res, next) => {
if (!req.user) {
throw new UnauthorizedError();
}
if (roles.length > 0 && !roles.includes(req.user.role)) {
throw new ForbiddenError();
}
next();
};
}
module.exports = { authenticate, authorize };
src/models/User.js:数据模型(模拟)
javascript
// 模拟数据库
const users = new Map();
let nextId = 1;
class User {
static findAll(filters = {}) {
let result = Array.from(users.values());
// 应用过滤条件
if (filters.status) {
result = result.filter(u => u.status === filters.status);
}
if (filters.role) {
result = result.filter(u => u.role === filters.role);
}
if (filters.search) {
const search = filters.search.toLowerCase();
result = result.filter(u =>
u.name.toLowerCase().includes(search) ||
u.email.toLowerCase().includes(search)
);
}
// 排序
if (filters.sort) {
const order = filters.order === 'desc' ? -1 : 1;
result.sort((a, b) => {
if (a[filters.sort] < b[filters.sort]) return -1 * order;
if (a[filters.sort] > b[filters.sort]) return 1 * order;
return 0;
});
}
return result;
}
static findById(id) {
return users.get(id);
}
static create(data) {
const id = nextId++;
const user = {
id,
...data,
status: 'active',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
users.set(id, user);
return user;
}
static update(id, data) {
const user = users.get(id);
if (!user) return null;
const updated = {
...user,
...data,
id, // 确保id不可修改
updated_at: new Date().toISOString()
};
users.set(id, updated);
return updated;
}
static delete(id) {
return users.delete(id);
}
}
module.exports = User;
src/controllers/userController.js:控制器
javascript
const User = require('../models/User');
const { ValidationError, NotFoundError } = require('../utils/errors');
// 获取用户列表
async function getUsers(req, res) {
const filters = {
status: req.query.status,
role: req.query.role,
search: req.query.search,
sort: req.query.sort || 'created_at',
order: req.query.order || 'desc'
};
const page = parseInt(req.query.page) || 1;
const pageSize = Math.min(parseInt(req.query.page_size) || 20, 100);
const allUsers = User.findAll(filters);
const total = allUsers.length;
const totalPages = Math.ceil(total / pageSize);
const start = (page - 1) * pageSize;
const users = allUsers.slice(start, start + pageSize);
res.json({
success: true,
data: {
items: users,
pagination: {
page,
page_size: pageSize,
total,
total_pages: totalPages,
has_next: page < totalPages,
has_prev: page > 1
}
}
});
}
// 获取单个用户
async function getUser(req, res) {
const user = User.findById(parseInt(req.params.id));
if (!user) {
throw new NotFoundError('user', req.params.id);
}
res.json({
success: true,
data: user
});
}
// 创建用户
async function createUser(req, res) {
const { name, email, password, role } = req.body;
// 验证
if (!name || !email || !password) {
throw new ValidationError('Missing required fields', [
{ field: 'name', message: 'Name is required' },
{ field: 'email', message: 'Email is required' },
{ field: 'password', message: 'Password is required' }
]);
}
// 简单邮箱格式验证
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new ValidationError('Invalid email format');
}
// 密码长度验证
if (password.length < 6) {
throw new ValidationError('Password too short');
}
const user = User.create({ name, email, password, role });
res.status(201).json({
success: true,
data: user
});
}
// 更新用户
async function updateUser(req, res) {
const id = parseInt(req.params.id);
// 检查用户是否存在
const existingUser = User.findById(id);
if (!existingUser) {
throw new NotFoundError('user', id);
}
// 邮箱格式验证(如果提供了email)
if (req.body.email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(req.body.email)) {
throw new ValidationError('Invalid email format');
}
}
const user = User.update(id, req.body);
res.json({
success: true,
data: user
});
}
// 删除用户
async function deleteUser(req, res) {
const id = parseInt(req.params.id);
const deleted = User.delete(id);
if (!deleted) {
throw new NotFoundError('user', id);
}
res.status(204).send();
}
module.exports = {
getUsers,
getUser,
createUser,
updateUser,
deleteUser
};
src/routes/users.js:路由
javascript
const express = require('express');
const router = express.Router();
const { authenticate, authorize } = require('../middleware/auth');
const userController = require('../controllers/userController');
// 公开路由
router.get('/', userController.getUsers);
router.get('/:id', userController.getUser);
// 需要认证的路由
router.post('/', authenticate, authorize('admin'), userController.createUser);
router.put('/:id', authenticate, authorize('admin'), userController.updateUser);
router.delete('/:id', authenticate, authorize('admin'), userController.deleteUser);
module.exports = router;
src/app.js:主应用
javascript
const express = require('express');
const errorHandler = require('./middleware/errorHandler');
const usersRouter = require('./routes/users');
const app = express();
// 中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 请求日志(简单版)
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next();
});
// 路由
app.use('/api/users', usersRouter);
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 404处理
app.use((req, res) => {
res.status(404).json({
success: false,
error: {
code: 'NOT_FOUND',
message: `Route ${req.method} ${req.url} not found`
}
});
});
// 错误处理
app.use(errorHandler);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;
运行测试
bash
# 安装依赖
npm install express jsonwebtoken
# 启动服务
node src/app.js
测试接口:
bash
# 健康检查
curl http://localhost:3000/health
# 获取用户列表
curl http://localhost:3000/api/users
# 创建用户(需要token,这里简化处理)
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_token_here" \
-d '{"name":"张三","email":"zhangsan@example.com","password":"123456"}'
# 获取单个用户
curl http://localhost:3000/api/users/1
# 更新用户
curl -X PUT http://localhost:3000/api/users/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_token_here" \
-d '{"name":"张三改"}'
# 删除用户
curl -X DELETE http://localhost:3000/api/users/1 \
-H "Authorization: Bearer your_token_here"
文档与调试
API文档
好的文档比代码还重要。推荐使用OpenAPI(Swagger)规范:
yaml
# openapi.yaml
openapi: 3.0.0
info:
title: User Management API
version: 1.0.0
description: 用户管理API接口文档
paths:
/users:
get:
summary: 获取用户列表
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: page_size
in: query
schema:
type: integer
default: 20
responses:
'200':
description: 成功
content:
application/json:
schema:
$ref: '#/components/schemas/UserList'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
status:
type: string
enum: [active, inactive]
常用调试工具
- Postman:功能强大的API测试工具
- Insomnia:轻量级API客户端
- curl:命令行工具,适合快速测试
- 浏览器开发者工具:Network面板
总结
好了,RESTful API设计规范就讲到这里。回顾一下今天学的内容:
- REST核心思想:URL表示资源,HTTP方法表示操作
- URL设计:用名词不用动词,复数形式,层级清晰
- HTTP方法:GET/POST/PUT/PATCH/DELETE的正确使用
- 状态码:合理使用2xx/4xx/5xx状态码
- 响应格式:统一的JSON结构
- 认证授权:Bearer Token+JWT
- 分页排序:多种分页方式,清晰的参数设计
- 错误处理:规范化错误码和错误格式
- 版本管理:URL路径版本控制
- 文档:使用OpenAPI规范
说真的,设计好的API不是一蹴而就的事,需要在实际项目中不断打磨。我的建议是:
- 先设计再写代码,别边写边改
- 多看看优秀API的设计(GitHub、Stripe都是好例子)
- 站在调用方的角度思考用户体验
- 文档和代码同样重要
- 保持一致性
关于内链方面,你可以继续学习TypeScript入门完全指南,用TypeScript能写出更健壮的API后端代码。或者学习Vue3 Composition API实战教程,了解前端如何调用这些API。
常见问题
Q:GET请求能不能带body?
A:技术上可以,但主流做法是不这么做。很多HTTP客户端和代理会忽略GET请求的body。建议GET请求的参数都用query string传递。
Q:PUT和PATCH有什么区别?
A:PUT要求提供完整资源数据,缺失字段会被清空或设为默认值。PATCH只需要提供要修改的字段,其他字段保持不变。
Q:POST和PUT都能创建资源,选哪个?
A:POST用于创建资源,服务器自动生成ID。PUT用于创建或替换资源,通常客户端指定ID。在实际项目中,POST用于创建,PUT用于更新。
Q:如何设计批量操作的API?
A:可以用数组参数:
bash
POST /users/batch
{
"operations": [
{"action": "create", "data": {...}},
{"action": "update", "id": 123, "data": {...}},
{"action": "delete", "id": 456}
]
}
Q:API应该返回多少数据?字段是否需要过滤?
A:建议支持字段选择:
bash
GET /users?fields=id,name,email
只返回需要的字段,减少网络传输量。
Q:什么时候用状态码,什么时候用错误码?
A:HTTP状态码表示请求级别的结果(成功、认证失败、资源不存在等),应用错误码表示业务逻辑的错误(余额不足、库存不足等)。两者配合使用。
希望这篇教程对你有帮助。如果有问题或建议,欢迎在评论区交流!

















