Node.js RESTful API 实战:从设计到实现

小飞兽 Node.js 335 次阅读 2026-07-28

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>