Node.js RESTful API 实战:从设计到实现
RESTful API 设计原则
REST(Representational State Transfer)是一种 API 设计风格,核心原则:
1. 资源命名 — 用名词表示资源,如 /users、/posts
2. HTTP 方法 — GET(查)、POST(增)、PUT/PATCH(改)、DELETE(删)
3. 状态码 — 正确使用 HTTP 状态码表示结果
4. 无状态 — 每个请求包含所有必要信息
项目结构
src/
├── routes/ # 路由层
│ ├── index.js
│ ├── users.js
│ └── posts.js
├── controllers/ # 控制器(处理请求逻辑)
├── services/ # 业务逻辑层
├── models/ # 数据模型
├── middlewares/ # 中间件
├── utils/ # 工具函数
├── config/ # 配置文件
└── app.js # 应用入口
完整 API 实现
// app.js
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const { errorHandler } = require('./middlewares/errorHandler');
const userRoutes = require('./routes/users');
const postRoutes = require('./routes/posts');
const app = express();
// 安全中间件
app.use(helmet());
app.use(cors({ origin: 'https://yourdomain.com' }));
// 限流:每个 IP 每 15 分钟最多 100 次请求
app.use('/api', rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { error: '请求过于频繁,请稍后再试' }
}));
// 解析 JSON
app.use(express.json({ limit: '10kb' })); // 限制请求体大小,防止攻击
// 请求日志
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next();
});
// 路由
app.use('/api/v1/users', userRoutes);
app.use('/api/v1/posts', postRoutes);
// 404
app.use((req, res) => {
res.status(404).json({ error: 'Not Found' });
});
// 错误处理
app.use(errorHandler);
module.exports = app;
// routes/posts.js
const express = require('express');
const router = express.Router();
const { body, param, query, validationResult } = require('express-validator');
const { authenticate } = require('../middlewares/auth');
// ===== 验证中间件 =====
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
// ===== 路由 =====
// GET /posts - 获取文章列表(支持分页、过滤、排序)
router.get('/',
[
query('page').optional().isInt({ min: 1 }).toInt(),
query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
query('sort').optional().isIn(['createdAt', 'title', 'views']),
query('order').optional().isIn(['asc', 'desc']),
query('authorId').optional().isInt().toInt(),
query('tag').optional().trim().escape(),
],
validate,
async (req, res, next) => {
try {
const {
page = 1,
limit = 10,
sort = 'createdAt',
order = 'desc',
authorId,
tag
} = req.query;
// 构建查询条件
const filter = {};
if (authorId) filter.authorId = authorId;
if (tag) filter.tags = tag;
// 模拟数据库查询
const posts = await Post.find(filter)
.sort({ [sort]: order })
.skip((page - 1) * limit)
.limit(limit)
.select('-__v');
const total = await Post.countDocuments(filter);
// 返回超媒体链接(利于客户端发现其他资源)
const baseUrl = `${req.protocol}://${req.get('host')}/api/v1/posts`;
res.json({
data: posts,
links: {
self: `${baseUrl}?${req.originalUrl.split('?')[1] || ''}`,
first: `${baseUrl}?page=1&limit=${limit}`,
last: `${baseUrl}?page=${Math.ceil(total / limit)}&limit=${limit}`,
next: page < Math.ceil(total / limit)
? `${baseUrl}?page=${page + 1}&limit=${limit}`
: null,
prev: page > 1 ? `${baseUrl}?page=${page - 1}&limit=${limit}` : null,
},
meta: { total, page, limit, pages: Math.ceil(total / limit) }
});
} catch (err) {
next(err);
}
}
);
// GET /posts/:id - 获取单篇文章
router.get('/:id',
[param('id').isInt().toInt()],
validate,
async (req, res, next) => {
try {
const post = await Post.findById(req.params.id).populate('author', 'name email');
if (!post) {
return res.status(404).json({ error: '文章不存在' });
}
res.json({ data: post });
} catch (err) {
next(err);
}
}
);
// POST /posts - 创建文章(需认证)
router.post('/',
authenticate,
[
body('title').trim().isLength({ min: 1, max: 200 }).escape(),
body('content').trim().isLength({ min: 1 }),
body('tags').optional().isArray(),
body('tags.*').trim().escape(),
],
validate,
async (req, res, next) => {
try {
const { title, content, tags } = req.body;
const post = await Post.create({
title,
content,
tags,
authorId: req.user.id
});
res.status(201).json({ data: post });
} catch (err) {
next(err);
}
}
);
// PATCH /posts/:id - 部分更新
router.patch('/:id',
authenticate,
[param('id').isInt().toInt()],
validate,
async (req, res, next) => {
try {
const post = await Post.findById(req.params.id);
if (!post) return res.status(404).json({ error: '文章不存在' });
// 只能修改自己的文章
if (post.authorId !== req.user.id) {
return res.status(403).json({ error: '无权修改此文章' });
}
const allowedUpdates = ['title', 'content', 'tags'];
const updates = {};
allowedUpdates.forEach(field => {
if (req.body[field] !== undefined) {
updates[field] = req.body[field];
}
});
Object.assign(post, updates);
await post.save();
res.json({ data: post });
} catch (err) {
next(err);
}
}
);
// DELETE /posts/:id - 删除文章
router.delete('/:id',
authenticate,
[param('id').isInt().toInt()],
validate,
async (req, res, next) => {
try {
const post = await Post.findById(req.params.id);
if (!post) return res.status(404).json({ error: '文章不存在' });
if (post.authorId !== req.user.id) {
return res.status(403).json({ error: '无权删除此文章' });
}
await post.deleteOne();
res.status(204).send();
} catch (err) {
next(err);
}
}
);
module.exports = router;
HTTP 状态码规范
| 状态码 | 含义 | 适用场景 |
|--------|------|---------|
| 200 | OK | 成功获取/更新资源 |
| 201 | Created | 成功创建资源 |
| 204 | No Content | 成功删除(无返回体) |
| 400 | Bad Request | 请求参数错误、验证失败 |
| 401 | Unauthorized | 未认证(未登录) |
| 403 | Forbidden | 已认证但无权限 |
| 404 | Not Found | 资源不存在 |
| 409 | Conflict | 资源冲突(如重复创建) |
| 422 | Unprocessable Entity | 语义错误(验证逻辑失败) |
| 429 | Too Many Requests | 请求过于频繁 |
| 500 | Internal Server Error | 服务器内部错误 |
常见问题
Q1: PUT 和 PATCH 有什么区别?
PUT 是完整替换,用新资源完全替代旧资源;PATCH 是部分更新,只修改提供的字段。在实际项目中,PATCH 更常用(性能好、网络传输少),但需要做好幂等性处理。
Q2: 如何设计 API 版本管理?
推荐使用 URL 路径方式,如 /api/v1/users、/api/v2/users,优点是直观、易调试、便于路由配置。Header 方式(如 Accept: application/vnd.api.v2+json)更符合 REST 规范但不够直观。
Q3: 如何防止 API 被恶意爬取?
1. 接口限流(express-rate-limit)限制单 IP 请求频率
2. 签名验证(如微信支付 API 用的 HMAC 签名)
3. 敏感数据加密传输
4. 异常检测:同一 token 短时间内大量请求则风控
5. 关键接口加 CAPTCHA 验证
延伸阅读
- <a href="https://restfulapi.cn/">RESTful API 最佳实践</a>
- <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status">HTTP 状态码完整指南</a>
- <a href="https://express-validator.github.io/docs/">express-validator 文档</a>