随着应用规模的增长,良好的项目结构变得至关重要。一个清晰、一致的目录组织不仅可以提高代码的可读性和可维护性,还能让新成员快速上手。本章将介绍构建 Node.js 项目时推荐的项目结构、分层原则以及常见模式的实现。
糟糕的项目结构会导致:
好的结构遵循关注点分离、单一职责和模块化原则,使应用更具弹性。
__tests__ 或与源码并列。一个典型的 Express + MongoDB 项目结构可能如下:
这个结构遵循了关注点分离的原则,每个目录有明确的职责。
集中管理所有配置,如数据库连接、第三方 API 密钥、应用设置。推荐使用 dotenv 加载环境变量,并提供一个配置对象导出。
// src/config/index.js
require('dotenv').config();
module.exports = {
port: process.env.PORT || 3000,
database: {
url: process.env.DB_URL || 'mongodb://localhost:27017/myapp',
options: { useNewUrlParser: true, useUnifiedTopology: true }
},
jwtSecret: process.env.JWT_SECRET,
logLevel: process.env.LOG_LEVEL || 'info'
};
定义数据结构和数据库交互。使用 ODM/ORM(如 Mongoose、Sequelize)时,模型通常放在这里。
// src/models/user.model.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
module.exports = mongoose.model('User', userSchema);
封装核心业务逻辑,避免在控制器中直接处理复杂业务。服务层可调用模型、外部 API、工具函数等。
// src/services/user.service.js
const User = require('../models/user.model');
class UserService {
async createUser(userData) {
const existing = await User.findOne({ email: userData.email });
if (existing) throw new Error('邮箱已存在');
const user = new User(userData);
await user.save();
return user;
}
// 其他方法...
}
module.exports = new UserService();
接收请求,调用服务,处理响应(包括错误处理和状态码)。不应包含业务逻辑。
// src/controllers/user.controller.js
const userService = require('../services/user.service');
exports.createUser = async (req, res, next) => {
try {
const user = await userService.createUser(req.body);
res.status(201).json({ success: true, data: user });
} catch (err) {
next(err); // 传递给错误处理中间件
}
};
将 HTTP 路径映射到对应的控制器方法。可以使用 express.Router 组织。
// src/routes/user.routes.js
const express = require('express');
const { createUser } = require('../controllers/user.controller');
const router = express.Router();
router.post('/', createUser);
// 其他路由...
module.exports = router;
然后在 app.js 中集中挂载所有路由模块:
// src/app.js
const express = require('express');
const userRoutes = require('./routes/user.routes');
// ... 其他中间件
const app = express();
app.use(express.json());
app.use('/api/users', userRoutes);
// ... 错误处理中间件
module.exports = app;
身份验证、日志、请求解析等中间件。
// src/middleware/auth.js
module.exports = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: '未授权' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch {
res.status(401).json({ error: '无效的 token' });
}
};
诸如密码哈希、日期格式化、日志封装等。
// src/utils/logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({ format: winston.format.simple() }));
}
module.exports = logger;
可以使用 express-validator 或 joi 定义验证规则。
// src/validations/user.validation.js
const Joi = require('joi');
exports.createUserSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
});
然后在控制器中使用验证中间件或手动验证。
通常分为两个文件:app.js 负责配置 Express 应用(不监听端口),server.js 负责启动服务器并连接数据库。这样便于测试时导入 app 而不启动端口。
// server.js
const mongoose = require('mongoose');
const app = require('./src/app');
const config = require('./src/config');
mongoose.connect(config.database.url, config.database.options)
.then(() => {
app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`);
});
})
.catch(err => {
console.error('Database connection failed:', err);
process.exit(1);
});
使用 dotenv 管理不同环境的配置。在项目根目录创建 .env(不提交)和 .env.example(模板)。通过 process.env 访问。
推荐使用配置对象将环境变量集中导出,如上文的 config/index.js。
定义一个统一的错误处理中间件,捕获所有错误并返回标准格式的响应。还可以自定义错误类以区分错误类型。
// src/middleware/errorHandler.js
const logger = require('../utils/logger');
module.exports = (err, req, res, next) => {
logger.error(err.stack);
const status = err.status || 500;
res.status(status).json({
success: false,
message: err.message || '服务器内部错误'
});
};
在 app.js 中在所有路由之后使用该中间件。
测试文件可以放在 tests/ 目录,并保持与 src/ 类似的子目录结构。使用 Jest 时,可以在 package.json 中配置 testMatch 匹配 tests/**/*.test.js。
tests/
├── unit/
│ ├── services/
│ │ └── user.service.test.js
│ └── utils/
│ └── logger.test.js
├── integration/
│ └── api/
│ └── user.test.js
└── setup.js # 测试全局配置
对于大型项目,可以使用依赖注入容器(如 awilix)管理依赖,使模块更易于测试和替换。将服务、控制器等注册到容器中,从容器中获取实例。
如果项目非常大,可以按业务领域划分模块,每个模块有自己的路由、控制器、服务、模型。例如:
src/modules/
├── user/
│ ├── controllers/
│ ├── services/
│ ├── models/
│ └── routes/
├── product/
│ └── ...
└── order/
└── ...
这种结构称为“按组件划分”,内聚性更强。
/api/v1)。user.controller.js)。.env.example 同步必需的变量。