停止编写 Spaghetti API 路由 — 像专家一样构建你的 Express.js 应用
抱歉,我无法直接访问外部链接获取文章内容。请您把需要翻译的文本粘贴在这里,我会帮您翻译成简体中文,并保留原有的格式和代码块。
面条式 API 问题
每个开发者都经历过这种情况。你带着最好的意图启动一个新的 Express.js 项目:一个干净的 index.js,也许两个路由,还有一个梦想。然后三个月后,你打开文件,看到 800 行路由处理函数,层层叠叠的中间件,以及到处散落的数据库调用,像糟糕派对后的彩纸一样。
这就是 面条式 API 问题,它导致的项目失败比 bug 还多。
在本文中,我将带你了解一种经过实战检验的 Express.js 文件夹结构,它可以从周末的副项目扩展到生产级应用,而不会变成维护噩梦。
为什么结构比你想象的更重要
- 糟糕的结构不仅仅是审美问题。它会主动拖慢团队速度,使新人入职变得痛苦,并把“添加一个简单功能”变成三小时的考古挖掘。
- 良好的结构恰恰相反。它让代码放置的正确位置一目了然,分离关注点,使得某一区域的更改不会意外波及其他区域,并且让你在扩展团队和代码库时不至于四分五裂。
让我们从头开始构建。
文件夹结构
以下是我们将实现的结构:
my-api/
src/
config/
index.js
database.js
controllers/
userController.js
postController.js
middlewares/
auth.js
errorHandler.js
validate.js
models/
User.js
Post.js
routes/
index.js
userRoutes.js
postRoutes.js
services/
userService.js
postService.js
utils/
logger.js
response.js
app.js
server.js
第 1 层 – 配置
所有环境变量和配置都放在这里。不再有 process.env.DATABASE_URL 分散在 15 个文件中。
// src/config/index.js
module.exports = {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
jwtSecret: process.env.JWT_SECRET,
db: {
url: process.env.DATABASE_URL,
poolSize: parseInt(process.env.DB_POOL_SIZE, 10) || 5,
},
};
当你的数据库 URL 变化时,只需更新 一个 文件。完成。
第 2 层 – 路由
路由是流量指挥员。它们不应包含 业务逻辑——只负责将 HTTP 方法/路径映射到控制器函数。
// src/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middlewares/auth');
const { validateCreateUser } = require('../middlewares/validate');
const userController = require('../controllers/userController');
router.get('/', authenticate, userController.getUsers);
router.post('/', validateCreateUser, userController.createUser);
router.get('/:id', authenticate, userController.getUserById);
router.put('/:id', authenticate, userController.updateUser);
router.delete('/:id', authenticate, userController.deleteUser);
module.exports = router;
所有路由组在单个 index 文件中组合:
// src/routes/index.js
const express = require('express');
const router = express.Router();
const userRoutes = require('./userRoutes');
const postRoutes = require('./postRoutes');
router.use('/users', userRoutes);
router.use('/posts', postRoutes);
module.exports = router;
第 3 层 – 控制器
控制器处理 HTTP 层:它们接收请求,调用相应的服务,并返回响应。没有数据库调用,也没有复杂逻辑。
// src/controllers/userController.js
const userService = require('../services/userService');
const { sendSuccess, sendError } = require('../utils/response');
exports.getUsers = async (req, res, next) => {
try {
const { page = 1, limit = 20 } = req.query;
const users = await userService.getUsers({ page, limit });
return sendSuccess(res, users);
} catch (error) {
next(error);
}
};
exports.createUser = async (req, res, next) => {
try {
const user = await userService.createUser(req.body);
return sendSuccess(res, user, 201);
} catch (error) {
next(error);
}
};
exports.getUserById = async (req, res, next) => {
try {
const user = await userService.getUserById(req.params.id);
if (!user) return sendError(res, 'User not found', 404);
return sendSuccess(res, user);
} catch (error) {
next(error);
}
};
第 4 层 – 服务
业务逻辑位于此。服务是普通的 JavaScript——没有 req、没有 res、没有 HTTP 概念——因此极易测试。
// src/services/userService.js
const User = require('../models/User');
const { hashPassword } = require('../utils/crypto');
exports.getUsers = async ({ page, limit }) => {
const offset = (page - 1) * limit;
const users = await User.findAll({
limit: parseInt(limit, 10),
offset,
attributes: { exclude: ['password'] },
});
return users;
};
exports.createUser = async (userData) => {
const existing = await User.findOne({ where: { email: userData.email } });
if (existing) {
const error = new Error('Email already in use');
error.statusCode = 409;
throw error;
}
const hashed = await hashPasswo
```js
rd(userData.password);
const user = await User.create({ ...userData, password: hashed });
const { password, ...userWithoutPassword } = user.toJSON();
return userWithoutPassword;
};
exports.getUserById = async (id) => {
return User.findByPk(id, {
attributes: { exclude: ['password'] },
});
};
第 5 层 – 模型
定义数据结构和 ORM 映射(例如 Sequelize、Mongoose)。保持它们不包含与请求相关的代码。
// src/models/User.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const User = sequelize.define('User', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
email: { type: DataTypes.STRING, unique: true, allowNull: false },
password: { type: DataTypes.STRING, allowNull: false },
name: { type: DataTypes.STRING },
// ...other fields
});
module.exports = User;
第 6 层 – 中间件
在请求与控制器之间运行的可复用逻辑(认证、验证、错误处理等)。
// src/middlewares/auth.js
module.exports.authenticate = (req, res, next) => {
// 验证 JWT,向 req 附加用户等
next();
};
// src/middlewares/validate.js
module.exports.validateCreateUser = (req, res, next) => {
// 执行 Joi/Yup 验证
next();
};
// src/middlewares/errorHandler.js
module.exports = (err, req, res, next) => {
const status = err.statusCode || 500;
res.status(status).json({ error: err.message });
};
第 7 层 – 工具函数
不适合放在其他位置的辅助函数(日志、响应格式化、加密帮助等)。
// src/utils/response.js
exports.sendSuccess = (res, data, status = 200) => {
res.status(status).json({ success: true, data });
};
exports.sendError = (res, message, status = 500) => {
res.status(status).json({ success: false, error: message });
};
// src/utils/logger.js
const winston = require('winston');
module.exports = winston.createLogger({
transports: [new winston.transports.Console()],
});
将所有内容连接在一起
// app.js
const express = require('express');
const config = require('./src/config');
const routes = require('./src/routes');
const errorHandler = require('./src/middlewares/errorHandler');
const app = express();
app.use(express.json());
app.use('/api', routes);
app.use(errorHandler);
module.exports = app;
// server.js
const http = require('http');
const app = require('./app');
const config = require('./src/config');
const server = http.createServer(app);
server.listen(config.port, () => {
console.log(`🚀 Server running on http://localhost:${config.port}`);
});
TL;DR
- Config – 集中的环境设置。
- Routes – 纯粹的 HTTP 路径 → 控制器映射。
- Controllers – 轻量级请求/响应处理器。
- Services – 业务逻辑,完全与框架无关。
- Models – 数据库模式 / ORM 定义。
- Middlewares – 可复用的请求处理工具。
- Utils – 各种辅助工具(日志、响应格式化等)。
坚持这种分层方法,你的 Express.js 代码库将保持整洁、可测试且可扩展——不再是乱七八糟的代码。
// Example of a Sequelize query with excluded fields
User.findAll({
attributes: { exclude: ['password'] },
});
第5层:Middlewares
中间件处理横切关注点:身份验证、校验、限流、日志记录。保持它们小而专注。
// src/middlewares/auth.js
const jwt = require('jsonwebtoken');
const config = require('../config');
exports.authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
try {
const decoded = jwt.verify(token, config.jwtSecret);
req.user = decoded;
next();
} catch {
return res.status(401).json({ message: 'Invalid token' });
}
};
// src/middlewares/errorHandler.js
module.exports = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
if (process.env.NODE_ENV !== 'production') {
console.error(err.stack);
}
res.status(statusCode).json({
success: false,
message,
});
};
全局错误处理器是此设置中不为人知的英雄。注意每个控制器只调用 next(error) —— 这个唯一的中间件会捕获所有错误并统一格式化响应。
将所有内容串联起来
// app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const routes = require('./src/routes');
const errorHandler = require('./src/middlewares/errorHandler');
const app = express();
// Security and parsing
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// All routes under /api/v1
app.use('/api/v1', routes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Error handler must be last
app.use(errorHandler);
module.exports = app;
// server.js
const app = require('./app');
const config = require('./src/config');
app.listen(config.port, () => {
console.log(`Server running on port ${config.port} in ${config.nodeEnv} mode`);
});
将 app.js 与 server.js 分离是一个小而有力的做法。这意味着你的测试套件可以直接导入 app,而无需实际启动服务器,从而使集成测试更加简洁。
经验法则
当你不确定某件事应该放在哪里时,问问自己:
- 它是否涉及
req或res?→ Controller 或 middleware。 - 它是否包含业务逻辑?→ Service。
- 它是否与数据库交互并映射数据?→ Model。
- 它是否是 HTTP 路径?→ Route。
- 它是否在多个地方使用且没有副作用?→ Utility。
只要遵循这五个问题,你几乎总能把它放在正确的位置。
这能为您带来什么
- 测试变得轻松。 服务没有 HTTP 依赖,因此单元测试只是函数调用。
- 新开发者快速上手。 文件夹名称讲述了应用的故事。
- 功能是隔离的。 添加新资源意味着在每一层都添加文件——你永远不需要触及无关的代码。
- 调试更快。 当出现 bug 时,你可以根据问题类型准确知道该查看哪一层。
结构不是官僚主义。它是构建持久事物的方式。
开始使用此布局重构你的下一个 Express.js 项目,看看一切变得多么轻松。你将花更少的时间寻找代码,更多的时间编写代码。