别再像2015年那样写API

发布: (2025年12月28日 GMT+8 18:50)
6 min read
原文: Dev.to

Source: Dev.to

Introduction

我们已经进入2025年,仍有许多代码库把 API 仅仅视为返回 JSON 的简单“端点”。如果你的 API 设计仍停留在基本的 CRUD 路由上,你就会牺牲性能、可扩展性和开发者体验。

1. 默认情况下停止返回所有内容

问题

// 2015 mindset
app.get('/api/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
  res.json(user); // Returns 47 fields no one asked for
});

为什么这不好

  • 杀死移动端性能
  • 暴露内部数据结构
  • 强迫前端自行过滤
  • 使缓存几乎不可能

现代方法

app.get('/api/users/:id', async (req, res) => {
  const fields = req.query.fields?.split(',') || ['id', 'name', 'email'];
  const user = await db.users.findById(req.params.id, { select: fields });
  res.json(user);
});

让客户端只请求它需要的内容。GraphQL 多年前就教会我们这一点——REST 也可以做到。

2. 无限分页是犯罪

问题

app.get('/api/products', async (req, res) => {
  const products = await db.products.find(); // All 50,000 rows
  res.json(products);
});

为什么它很糟糕

  • 一次请求可能会让你的数据库崩溃
  • 无法应对增长
  • 超时会降低用户体验

现代方法(基于游标的分页)

app.get('/api/products', async (req, res) => {
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);
  const cursor = req.query.cursor;

  const products = await db.products.find({
    where: cursor ? { id: { gt: cursor } } : {},
    limit: limit + 1
  });

  const hasNext = products.length > limit;
  const items = hasNext ? products.slice(0, -1) : products;

  res.json({
    data: items,
    cursor: hasNext ? items[items.length - 1].id : null
  });
});

可预测的负载和不会破坏服务器的无限滚动。

3. 错误响应不是事后才考虑的

问题

app.post('/api/orders', async (req, res) => {
  try {
    const order = await createOrder(req.body);
    res.json(order);
  } catch (err) {
    res.status(500).json({ error: 'Something went wrong' });
  }
});

前端开发者讨厌这种模糊的响应。

现代做法

app.post('/api/orders', async (req, res) => {
  try {
    const order = await createOrder(req.body);
    res.json({ data: order });
  } catch (err) {
    if (err.name === 'ValidationError') {
      return res.status(400).json({
        error: {
          code: 'VALIDATION_FAILED',
          message: 'Invalid order data',
          fields: err.details
        }
      });
    }

    if (err.name === 'InsufficientStock') {
      return res.status(409).json({
        error: {
          code: 'INSUFFICIENT_STOCK',
          message: 'Product out of stock',
          productId: err.productId
        }
      });
    }

    // Log actual error server‑side
    logger.error(err);
    res.status(500).json({
      error: {
        code: 'INTERNAL_ERROR',
        message: 'Failed to create order'
      }
    });
  }
});

结构化的错误提供可操作的反馈,使前端能够优雅地处理失败。

4. 限流不再是可选项

问题
没有限流会让你的 API 暴露于滥用、意外失控的脚本以及高成本的流量峰值。

现代做法

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  handler: (req, res) => {
    res.status(429).json({
      error: {
        code: 'RATE_LIMIT_EXCEEDED',
        message: 'Too many requests',
        retryAfter: req.rateLimit.resetTime
      }
    });
  }
});

app.use('/api/', limiter);

保护你的基础设施——这在 2025 年应当成为默认设置。

5. 从第一天起的版本管理

问题
app.get('/api/users', …); – 当接口契约改变时会怎样?

现代做法

app.get('/api/v1/users', …);   // 现有客户端
app.get('/api/v2/users', …);   // 新行为

版本管理并非过早的优化;它尊重你的用户,也照顾未来的自己。

6. 停止忽视 HTTP 状态码

问题

res.status(200).json({ error: 'User not found' }); // WRONG

现代做法 – 使用合适的状态码:

  • 200 – 成功
  • 201 – 已创建
  • 400 – 错误请求(客户端错误)
  • 401 – 未授权
  • 403 – 禁止访问
  • 404 – 未找到
  • 409 – 冲突(重复、约束违规)
  • 422 – 不可处理的实体(验证失败)
  • 429 – 限流
  • 500 – 服务器错误(你的责任)

正确的状态码使客户端、监控工具和缓存层能够正常工作。

7. 缓存头是免费性能

问题
每个请求都会访问数据库,即使是几周未更改的数据也是如此。

现代方法

app.get('/api/products/:id', async (req, res) => {
  const product = await db.products.findById(req.params.id);
  res.set({
    'Cache-Control': 'public, max-age=300', // 5 minutes
    'ETag': generateETag(product)
  });
  res.json(product);
});

CDN、浏览器和代理会为您处理繁重的工作。

传统 API 模式的真实成本

  • 单个未优化的端点每月可能产生 $800/月 的数据库读取费用。
  • 缺少速率限制曾导致一个有缺陷的移动应用意外触发 DDoS 攻击。
  • 糟糕的错误处理把每个 bug 报告都变成了 “出现了问题”。

现代 API 设计并非追求潮流,而是构建能够扩展、能够容忍用户错误,并且在两年后无需完全重写的系统。

在 2025 年你仍然看到哪些过时的 API 模式?在评论中留下你的见解。

Back to Blog

相关文章

阅读更多 »