RESTful API 设计规范与最佳实践:从理论到实战

By | 2026年6月16日

RESTful API 设计规范与最佳实践:从理论到实战

摘要

RESTful API已成为现代Web服务的标准架构风格。本文将从资源命名、HTTP方法、状态码、版本控制、安全等方面,结合Node.js+Express实战,带你掌握RESTful API的设计精髓。

1. 引言

你是否遇到过这样的API:/getUser?id=1/deleteUser?id=2/createUser?这种RPC风格的接口不仅混乱,而且难以维护。RESTful API通过资源导向和标准HTTP方法,提供了统一、可预测的接口设计。本文不仅会列出规范,还会通过一个简单的任务管理系统,手把手教你实现RESTful API。

2. 核心原则

2.1 资源导向

URL代表资源(名词),而非操作(动词)。例如:

  • 错误:/getUser/createUser
  • 正确:/users/users/123

2.2 使用标准HTTP方法

| 方法 | 操作 | 示例 | |——|——|——| | GET | 获取资源 | GET /usersGET /users/123 | | POST | 创建资源 | POST /users | | PUT | 完全替换资源 | PUT /users/123 | | PATCH | 部分更新资源 | PATCH /users/123 | | DELETE | 删除资源 | DELETE /users/123 |

2.3 状态码表达结果

  • 2xx 成功:200 OK, 201 Created, 204 No Content
  • 3xx 重定向:301 Moved Permanently, 304 Not Modified
  • 4xx 客户端错误:400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity
  • 5xx 服务器错误:500 Internal Server Error, 502 Bad Gateway

3. 实战:构建任务管理API

我们将使用Node.js + Express + 内存存储来演示。

3.1 项目初始化


mkdir task-api && cd task-api
npm init -y
npm install express

3.2 基础服务器


// server.js
const express = require('express');
const app = express();
app.use(express.json());

let tasks = [];
let idCounter = 1;

app.listen(3000, () => console.log('API running on port 3000'));

3.3 资源命名与端点设计

| 端点 | 方法 | 描述 | |——|——|——| | /tasks | GET | 获取所有任务 | | /tasks | POST | 创建新任务 | | /tasks/:id | GET | 获取单个任务 | | /tasks/:id | PUT | 完全替换任务 | | /tasks/:id | PATCH | 部分更新任务 | | /tasks/:id | DELETE | 删除任务 |

3.4 实现CRUD


// 获取所有任务
app.get('/tasks', (req, res) => {
  res.json(tasks);
});

// 创建任务
app.post('/tasks', (req, res) => {
  const { title, completed } = req.body;
  if (!title || typeof title !== 'string') {
    return res.status(400).json({ error: 'Title is required and must be a string' });
  }
  const task = {
    id: idCounter++,
    title,
    completed: completed || false,
    createdAt: new Date().toISOString()
  };
  tasks.push(task);
  res.status(201).json(task);
});

// 获取单个任务
app.get('/tasks/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const task = tasks.find(t => t.id === id);
  if (!task) return res.status(404).json({ error: 'Task not found' });
  res.json(task);
});

// 完全替换任务
app.put('/tasks/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = tasks.findIndex(t => t.id === id);
  if (index === -1) return res.status(404).json({ error: 'Task not found' });
  const { title, completed } = req.body;
  if (!title || typeof title !== 'string') {
    return res.status(400).json({ error: 'Title is required' });
  }
  tasks[index] = {
    id,
    title,
    completed: !!completed,
    createdAt: tasks[index].createdAt,
    updatedAt: new Date().toISOString()
  };
  res.json(tasks[index]);
});

// 部分更新任务
app.patch('/tasks/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const task = tasks.find(t => t.id === id);
  if (!task) return res.status(404).json({ error: 'Task not found' });
  const { title, completed } = req.body;
  if (title !== undefined) {
    if (typeof title !== 'string') return res.status(400).json({ error: 'Title must be a string' });
    task.title = title;
  }
  if (completed !== undefined) {
    if (typeof completed !== 'boolean') return res.status(400).json({ error: 'Completed must be a boolean' });
    task.completed = completed;
  }
  task.updatedAt = new Date().toISOString();
  res.json(task);
});

// 删除任务
app.delete('/tasks/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const index = tasks.findIndex(t => t.id === id);
  if (index === -1) return res.status(404).json({ error: 'Task not found' });
  tasks.splice(index, 1);
  res.status(204).send();
});

💡 注意:PUT应替换整个资源,因此需要提供所有必填字段;PATCH只更新提供的字段。

3.5 测试API

使用curl测试:


# 创建任务
curl -X POST http://localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"学习REST"}'

# 获取所有任务
curl http://localhost:3000/tasks

# 获取单个任务
curl http://localhost:3000/tasks/1

# 部分更新
curl -X PATCH http://localhost:3000/tasks/1 -H "Content-Type: application/json" -d '{"completed":true}'

# 删除任务
curl -X DELETE http://localhost:3000/tasks/1

4. 高级实践

4.1 过滤、排序与分页

使用查询参数:


// GET /tasks?completed=true&sort=-createdAt&page=1&limit=10
app.get('/tasks', (req, res) => {
  let result = [...tasks];
  
  // 过滤
  if (req.query.completed !== undefined) {
    const completed = req.query.completed === 'true';
    result = result.filter(t => t.completed === completed);
  }
  
  // 排序
  if (req.query.sort) {
    const fields = req.query.sort.split(',');
    fields.forEach(field => {
      const order = field.startsWith('-') ? -1 : 1;
      const key = field.replace(/^-/, '');
      result.sort((a, b) => (a[key] > b[key] ? order : -order));
    });
  }
  
  // 分页
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const start = (page - 1) * limit;
  const paginated = result.slice(start, start + limit);
  
  res.json({
    data: paginated,
    total: result.length,
    page,
    limit
  });
});

4.2 版本控制

推荐使用URL路径版本:


app.use('/v1', v1Router);
app.use('/v2', v2Router);

或者请求头版本(更优雅但不易发现):


app.use((req, res, next) => {
  const version = req.headers['accept-version'];
  req.apiVersion = version || '1';
  next();
});

4.3 错误处理

统一错误响应格式:


// 全局错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    error: 'Internal Server Error',
    message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong'
  });
});

4.4 认证与授权

使用JWT示例:


const jwt = require('jsonwebtoken');

// 中间件验证
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token provided' });
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    res.status(403).json({ error: 'Invalid token' });
  }
}

app.get('/tasks', authenticate, (req, res) => {
  // 只返回当前用户的任务
  const userTasks = tasks.filter(t => t.userId === req.user.id);
  res.json(userTasks);
});

4.5 幂等性

PUT和DELETE应该是幂等的。例如,多次DELETE同一个资源应返回相同结果(404)。

5. 常见陷阱

  • 使用动词:不要用/getTasks,用GET /tasks
  • 忽略状态码:总是返回适当的状态码,不要所有请求都返回200
  • 嵌套过深:避免/users/1/tasks/2/comments,考虑扁平化或使用查询参数
  • 忽略HATEOAS:虽然不是强制,但返回相关链接可以提高API的可发现性

6. 总结

本文从理论到实战,带你构建了符合RESTful原则的任务管理API。关键点:

  • 资源命名使用复数名词
  • 利用HTTP方法表达操作
  • 正确使用状态码
  • 实现过滤、排序、分页
  • 版本控制与安全

下一步,你可以考虑添加HATEOAS链接,或者使用OpenAPI规范文档化API。

延伸阅读