别再像2015年那样写API
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 模式?在评论中留下你的见解。