项目结构
概述
正确组织 Node.js 项目对于可维护性、可扩展性和团队协作至关重要。本章介绍 Node.js 应用程序的结构化最佳实践,从简单脚本到复杂的企业级应用程序。
基础项目结构
简单应用程序结构
对于小型应用或学习项目:
my-node-app/
├── package.json
├── package-lock.json
├── .gitignore
├── .env
├── README.md
├── app.js # Main application file
├── config/
│ └── database.js # Configuration files
├── routes/
│ ├── index.js # Route definitions
│ └── users.js
├── models/
│ └── user.js # Data models
├── views/
│ └── index.html # Templates (if using)
├── public/
│ ├── css/
│ ├── js/
│ └── images/
└── tests/
└── app.test.js # Test files中型应用程序结构
对于生产应用程序:
enterprise-app/
├── package.json
├── package-lock.json
├── .gitignore
├── .env.example
├── .eslintrc.js
├── .prettierrc
├── README.md
├── Dockerfile
├── docker-compose.yml
├── src/
│ ├── app.js # Application entry point
│ ├── server.js # Server setup
│ ├── config/
│ │ ├── index.js # Configuration management
│ │ ├── database.js
│ │ └── redis.js
│ ├── controllers/
│ │ ├── authController.js
│ │ └── userController.js
│ ├── middleware/
│ │ ├── auth.js
│ │ ├── validation.js
│ │ └── errorHandler.js
│ ├── models/
│ │ ├── index.js
│ │ ├── User.js
│ │ └── Product.js
│ ├── routes/
│ │ ├── index.js
│ │ ├── auth.js
│ │ └── api/
│ │ ├── v1/
│ │ │ ├── index.js
│ │ │ ├── users.js
│ │ │ └── products.js
│ ├── services/
│ │ ├── authService.js
│ │ ├── emailService.js
│ │ └── paymentService.js
│ ├── utils/
│ │ ├── logger.js
│ │ ├── helpers.js
│ │ └── constants.js
│ └── validators/
│ ├── userValidator.js
│ └── productValidator.js
├── tests/
│ ├── unit/
│ ├── integration/
│ └── fixtures/
├── docs/
│ ├── api.md
│ └── deployment.md
├── scripts/
│ ├── seed.js
│ └── migrate.js
└── logs/
└── .gitkeep配置管理
基于环境的配置
创建健壮的配置系统:
javascript
// src/config/index.js
const dotenv = require('dotenv');
const path = require('path');
// Load environment variables
dotenv.config();
const config = {
// Environment
NODE_ENV: process.env.NODE_ENV || 'development',
// Server
PORT: parseInt(process.env.PORT, 10) || 3000,
HOST: process.env.HOST || 'localhost',
// Database
DATABASE: {
HOST: process.env.DB_HOST || 'localhost',
PORT: parseInt(process.env.DB_PORT, 10) || 5432,
NAME: process.env.DB_NAME || 'myapp',
USER: process.env.DB_USER || 'postgres',
PASSWORD: process.env.DB_PASSWORD || '',
SSL: process.env.DB_SSL === 'true'
},
// Redis
REDIS: {
HOST: process.env.REDIS_HOST || 'localhost',
PORT: parseInt(process.env.REDIS_PORT, 10) || 6379,
PASSWORD: process.env.REDIS_PASSWORD || ''
},
// JWT
JWT: {
SECRET: process.env.JWT_SECRET || 'your-secret-key',
EXPIRES_IN: process.env.JWT_EXPIRES_IN || '24h'
},
// Email
EMAIL: {
SERVICE: process.env.EMAIL_SERVICE || 'gmail',
USER: process.env.EMAIL_USER || '',
PASSWORD: process.env.EMAIL_PASSWORD || ''
},
// File uploads
UPLOAD: {
MAX_SIZE: parseInt(process.env.UPLOAD_MAX_SIZE, 10) || 5 * 1024 * 1024, // 5MB
ALLOWED_TYPES: (process.env.UPLOAD_ALLOWED_TYPES || 'jpg,jpeg,png,pdf').split(',')
},
// Logging
LOG_LEVEL: process.env.LOG_LEVEL || 'info'
};
// Validation
function validateConfig() {
const required = ['JWT.SECRET'];
for (const key of required) {
const keys = key.split('.');
let value = config;
for (const k of keys) {
value = value[k];
}
if (!value) {
throw new Error(`Missing required configuration: ${key}`);
}
}
}
// Validate in production
if (config.NODE_ENV === 'production') {
validateConfig();
}
module.exports = config;环境文件
创建不同的环境文件:
bash
# .env.example
NODE_ENV=development
PORT=3000
HOST=localhost
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp_dev
DB_USER=postgres
DB_PASSWORD=password
DB_SSL=false
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# JWT
JWT_SECRET=your-super-secret-jwt-key
JWT_EXPIRES_IN=24h
# Email
EMAIL_SERVICE=gmail
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-app-password
# File uploads
UPLOAD_MAX_SIZE=5242880
UPLOAD_ALLOWED_TYPES=jpg,jpeg,png,pdf
# Logging
LOG_LEVEL=debug应用程序入口点
服务器设置
javascript
// src/server.js
const app = require('./app');
const config = require('./config');
const logger = require('./utils/logger');
const server = app.listen(config.PORT, config.HOST, () => {
logger.info(`Server running on ${config.HOST}:${config.PORT} in ${config.NODE_ENV} mode`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received. Shutting down gracefully...');
server.close(() => {
logger.info('Process terminated');
process.exit(0);
});
});
process.on('SIGINT', () => {
logger.info('SIGINT received. Shutting down gracefully...');
server.close(() => {
logger.info('Process terminated');
process.exit(0);
});
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
module.exports = server;应用程序设置
javascript
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const config = require('./config');
const logger = require('./utils/logger');
const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: config.NODE_ENV === 'production' ? ['https://yourdomain.com'] : true,
credentials: true
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Compression
app.use(compression());
// Request logging
app.use((req, res, next) => {
logger.info(`${req.method} ${req.path} - ${req.ip}`);
next();
});
// Static files
app.use(express.static('public'));
// Routes
app.use('/', routes);
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({
success: false,
message: 'Route not found'
});
});
// Error handling middleware
app.use(errorHandler);
module.exports = app;模块化架构
控制器
javascript
// src/controllers/userController.js
const userService = require('../services/userService');
const { validationResult } = require('express-validator');
const logger = require('../utils/logger');
class UserController {
async getAllUsers(req, res, next) {
try {
const { page = 1, limit = 10, search } = req.query;
const users = await userService.getAllUsers({
page: parseInt(page),
limit: parseInt(limit),
search
});
res.json({
success: true,
data: users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: users.total
}
});
} catch (error) {
next(error);
}
}
async getUserById(req, res, next) {
try {
const { id } = req.params;
const user = await userService.getUserById(id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
data: user
});
} catch (error) {
next(error);
}
}
async createUser(req, res, next) {
try {
// Check validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
const userData = req.body;
const user = await userService.createUser(userData);
logger.info(`User created: ${user.id}`);
res.status(201).json({
success: true,
data: user,
message: 'User created successfully'
});
} catch (error) {
next(error);
}
}
async updateUser(req, res, next) {
try {
const { id } = req.params;
const updateData = req.body;
const user = await userService.updateUser(id, updateData);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
data: user,
message: 'User updated successfully'
});
} catch (error) {
next(error);
}
}
async deleteUser(req, res, next) {
try {
const { id } = req.params;
const deleted = await userService.deleteUser(id);
if (!deleted) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
message: 'User deleted successfully'
});
} catch (error) {
next(error);
}
}
}
module.exports = new UserController();服务
javascript
// src/services/userService.js
const User = require('../models/User');
const bcrypt = require('bcrypt');
const logger = require('../utils/logger');
class UserService {
async getAllUsers({ page = 1, limit = 10, search }) {
try {
const offset = (page - 1) * limit;
let whereClause = {};
if (search) {
whereClause = {
$or: [
{ name: { $regex: search, $options: 'i' } },
{ email: { $regex: search, $options: 'i' } }
]
};
}
const [users, total] = await Promise.all([
User.find(whereClause)
.select('-password')
.skip(offset)
.limit(limit)
.sort({ createdAt: -1 }),
User.countDocuments(whereClause)
]);
return {
users,
total,
pages: Math.ceil(total / limit)
};
} catch (error) {
logger.error('Error fetching users:', error);
throw error;
}
}
async getUserById(id) {
try {
const user = await User.findById(id).select('-password');
return user;
} catch (error) {
logger.error(`Error fetching user ${id}:`, error);
throw error;
}
}
async createUser(userData) {
try {
// Check if user already exists
const existingUser = await User.findOne({ email: userData.email });
if (existingUser) {
throw new Error('User with this email already exists');
}
// Hash password
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(userData.password, saltRounds);
// Create user
const user = new User({
...userData,
password: hashedPassword
});
await user.save();
// Return user without password
const { password, ...userWithoutPassword } = user.toObject();
return userWithoutPassword;
} catch (error) {
logger.error('Error creating user:', error);
throw error;
}
}
async updateUser(id, updateData) {
try {
// Remove sensitive fields from update
const { password, ...safeUpdateData } = updateData;
const user = await User.findByIdAndUpdate(
id,
safeUpdateData,
{ new: true, runValidators: true }
).select('-password');
return user;
} catch (error) {
logger.error(`Error updating user ${id}:`, error);
throw error;
}
}
async deleteUser(id) {
try {
const user = await User.findByIdAndDelete(id);
return !!user;
} catch (error) {
logger.error(`Error deleting user ${id}:`, error);
throw error;
}
}
async getUserByEmail(email) {
try {
const user = await User.findOne({ email });
return user;
} catch (error) {
logger.error(`Error fetching user by email ${email}:`, error);
throw error;
}
}
}
module.exports = new UserService();中间件
javascript
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const config = require('../config');
const userService = require('../services/userService');
const authMiddleware = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
message: 'Access denied. No token provided.'
});
}
const decoded = jwt.verify(token, config.JWT.SECRET);
const user = await userService.getUserById(decoded.userId);
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid token. User not found.'
});
}
req.user = user;
next();
} catch (error) {
res.status(401).json({
success: false,
message: 'Invalid token.'
});
}
};
const adminMiddleware = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({
success: false,
message: 'Access denied. Admin privileges required.'
});
}
next();
};
module.exports = {
authMiddleware,
adminMiddleware
};工具和辅助函数
日志记录器
javascript
// src/utils/logger.js
const winston = require('winston');
const config = require('../config');
const logger = winston.createLogger({
level: config.LOG_LEVEL,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'node-app' },
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
// Add console transport in development
if (config.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
module.exports = logger;辅助函数
javascript
// src/utils/helpers.js
const crypto = require('crypto');
/**
* Generate random string
*/
function generateRandomString(length = 32) {
return crypto.randomBytes(length).toString('hex');
}
/**
* Sanitize user input
*/
function sanitizeInput(input) {
if (typeof input !== 'string') return input;
return input.trim().replace(/[<>]/g, '');
}
/**
* Format response
*/
function formatResponse(success, data = null, message = null, errors = null) {
const response = { success };
if (data !== null) response.data = data;
if (message !== null) response.message = message;
if (errors !== null) response.errors = errors;
return response;
}
/**
* Pagination helper
*/
function getPaginationData(page, limit, total) {
const totalPages = Math.ceil(total / limit);
const hasNext = page < totalPages;
const hasPrev = page > 1;
return {
page,
limit,
total,
totalPages,
hasNext,
hasPrev
};
}
/**
* Async wrapper for route handlers
*/
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
module.exports = {
generateRandomString,
sanitizeInput,
formatResponse,
getPaginationData,
asyncHandler
};Package.json 脚本
json
{
"name": "node-enterprise-app",
"version": "1.0.0",
"description": "Enterprise Node.js application",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"format": "prettier --write src/",
"seed": "node scripts/seed.js",
"migrate": "node scripts/migrate.js",
"build": "echo 'No build step required'",
"docker:build": "docker build -t node-app .",
"docker:run": "docker run -p 3000:3000 node-app"
},
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.0.0",
"bcrypt": "^5.1.0",
"jsonwebtoken": "^9.0.0",
"cors": "^2.8.5",
"helmet": "^6.0.1",
"compression": "^1.7.4",
"express-rate-limit": "^6.7.0",
"express-validator": "^6.15.0",
"winston": "^3.8.2",
"dotenv": "^16.0.3"
},
"devDependencies": {
"nodemon": "^2.0.22",
"jest": "^29.5.0",
"supertest": "^6.3.3",
"eslint": "^8.38.0",
"prettier": "^2.8.7"
}
}下一步
在下一章中,我们将详细探索配置管理,并学习如何有效地处理不同的环境。
实践练习
- 为博客应用程序创建项目结构
- 实现带验证的配置系统
- 构建模块化身份验证系统
- 创建用于日志记录和错误处理的可重用中间件
关键要点
- 适当的项目结构提高可维护性和可扩展性
- 使用控制器、服务和中间件分离关注点
- 为不同的部署场景使用基于环境的配置
- 实施适当的错误处理和日志记录
- 创建可重用的实用工具和辅助函数
- 遵循一致的命名约定和文件组织