MongoDB: Express与mongoose集成:Web应用架构
最后更新:2026-08-26
Express + mongoose 是 Node.js 生态最成熟的 Web 开发组合——掌握它能构建生产级 API。
Express 在 Node.js Web 框架中的定位:Node.js Web 框架分三个层级——1. 底层(http 模块):最轻量但需手动处理路由/解析/错误,不实际;2. 中层(Express/Koa):提供路由/中间件/错误处理等基础能力,灵活但需自行组装(ORM/校验/日志等需选型);3. 上层(NestJS/AdonisJS):内置完整方案(DI/ORM/校验/日志),开箱即用但学习曲线陡。Express 占据最大市场份额的原因——1. 极简内核(只做路由+中间件,其他自由选择);2. 中间件生态最丰富(npm 上 Express 中间件数量远超其他框架);3. 学习成本低(2 小时掌握核心 API)。选择建议:个人项目/小团队→Express(灵活快速),企业级项目→NestJS(规范完整)。
Express + mongoose 组合的优势与局限:这对组合的优势——1. 社区最大:遇到问题搜索答案最多,Stack Overflow 上 Express+mongoose 的问题覆盖面最广;2. 灵活性:Express 不绑定 ORM,可随时替换 mongoose 为 Prisma/TypeORM;3. 渐进式:从最简单的 app.get 开始,逐步添加中间件/分层/校验,学习曲线平缓。局限——1. 无类型安全(JavaScript 项目无编译时类型检查,需配合 TypeScript + 接口定义);2. 无内置规范(项目结构/命名/分层全靠团队约定,新手容易写出面条代码);3. 回调风格(Express 基于回调,async 错误需要包装或使用 express-async-errors)。理解这些局限有助于在项目初期制定技术规范来规避。
1. 你将学到
- Express 4.x 基础
- mongoose 连接池配置
- MVC 模式(Model / View / Controller)
- RESTful 路由设计
- 错误处理中间件
- dotenv 环境变量
2. Express 4.x 基础
什么是 Express? Express 是 Node.js 最流行的 Web 应用框架,提供了简洁的路由系统、中间件机制和 HTTP 工具方法。它不是一个"全栈框架",而是一个极简的路由与中间件层——这正是它的优势:灵活、可组合、生态丰富。
Express 核心设计原理:Express 基于"中间件栈"模型——每个 HTTP 请求经过一系列中间件函数,每个函数可以读取请求(req)、修改响应(res)、或把控制权传给下一个中间件(next)。这种洋葱模型使得功能可以像积木一样组合:日志、认证、验证、业务逻辑、错误处理各居其位。
Express 的中间件哲学:Express 的核心设计哲学是"极简+可组合"——框架本身只提供 HTTP 抽象和中间件机制,所有功能(路由、认证、验证、日志)都通过中间件实现。与 Django/Rails 的"全家桶"不同,Express 不内置 ORM、认证或模板引擎——开发者按需选择组件。优势:学习成本低、灵活度高、可替换性强。劣势:需要自己组装技术栈、缺乏统一规范、初学者容易选错组件。Express 适合"有经验的团队按需组装",Django/Rails 适合"小团队快速出活"。
技术栈选型依据:为什么选择 Express + mongoose?1. Express 生态最成熟(中间件数量远超 Koa/Fastify),遇到问题容易找到解决方案;2. mongoose 提供 Schema 验证、中间件、populate 等高级功能,比 native driver 减少大量样板代码;3. 学习曲线平缓——req/res 模型直观易懂;4. 社区活跃(npm 周下载量 Express 3000万+,mongoose 100万+)。Koa 更优雅(async/await 原生),Fastify 更快(性能优化),NestJS 更规范(Angular 风格),但 Express 的综合优势(生态+文档+人才)最适合教学和生产。
sequenceDiagram
participant Client
participant Express as Express Server
participant MW1 as 日志中间件
participant MW2 as 认证中间件
participant MW3 as 验证中间件
participant Ctrl as Controller
participant DB as MongoDB
Client->>Express: HTTP Request
Express->>MW1: req → res → next()
MW1->>MW2: next()
MW2->>MW3: 认证通过
MW3->>Ctrl: 验证通过
Ctrl->>DB: mongoose查询
DB-->>Ctrl: 查询结果
Ctrl-->>Client: JSON Response
Express vs 其他 Node.js 框架:
| 维度 | Express | Koa | Fastify | NestJS |
|---|---|---|---|---|
| 核心理念 | 中间件栈 | 洋葱模型 | 高性能插件 | 装饰器+DI |
| 性能 | 基准 | 略慢 | 2-3x 快 | 略慢 |
| 生态成熟度 | ★★★★★ | ★★★ | ★★★ | ★★★★ |
| 学习曲线 | 低 | 低 | 中 | 高 |
| TypeScript | 需配置 | 需配置 | 原生支持 | 原生支持 |
| 适用场景 | 通用API | 轻量服务 | 高并发API | 企业应用 |
为什么 Express + mongoose 是最佳组合? Express 的 req/res 模型与 mongoose 的异步查询天然契合——Controller 函数接收 req、查询 mongoose、返回 res.json(),代码直观且可维护。
Express 生态的关键中间件:Express 的极简设计意味着核心只做路由,其他功能通过中间件补充——1. 安全:helmet(设置安全 HTTP 头)、cors(跨域资源共享)、express-rate-limit(请求限流);2. 解析:express.json()(解析 JSON 请求体)、express.urlencoded()(解析表单数据)、multer(文件上传);3. 日志:morgan(HTTP 请求日志)、winston(应用日志);4. 压缩:compression(gzip 压缩响应体,减少带宽 60%+);5. 健康检查:express-healthcheck(/health 端点,负载均衡器探测)。生产应用应至少安装 helmet + cors + morgan + compression,它们是基线安全与性能保障。
(1) 安装依赖
npm install express mongoose dotenv
(2) 基本应用
// === 基本 Express 应用 ===
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.get('/', (req, res) => {
res.json({ message: 'Welcome to ShopHub API' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
3. mongoose 连接配置
mongoose 连接池原理:mongoose 默认使用 MongoDB Node.js 驱动的连接池。连接池维护一组已建立的 TCP 连接,新请求复用空闲连接而非每次新建——避免了 TCP 三次握手和 MongoDB 认证的开销。连接池大小决定了应用能并发执行多少数据库操作。
连接池的生命周期:连接池中的每个连接经历"创建→空闲→活跃→空闲→关闭"的生命周期——1. 应用启动时创建 minPoolSize 个连接(预热,避免首次请求延迟);2. 请求到来时从池中借用空闲连接(活跃状态);3. 请求完成归还连接(空闲状态);4. 空闲超时(maxIdleTimeMS)后关闭连接(节省资源);5. 连接池耗尽时新请求进入等待队列(waitQueueTimeoutMS 后超时报错)。理解生命周期有助于诊断连接泄漏——如果活跃连接数持续增长不归还,说明有请求未正确释放连接(通常是忘记 await 或异常未捕获)。
连接池与 Serverless 的冲突:传统连接池假设应用长时间运行、连接可复用。Serverless(如 AWS Lambda)每次调用是一个新进程——连接池无法跨调用复用,每次冷启动都建立新连接。解决方案:1. 使用 MongoDB Atlas Serverless(自动管理连接);2. 在 Lambda handler 外部初始化 mongoose(复用容器实例的连接);3. 使用连接管理中间件(如 mongoose 连接缓存);4. 减小 maxPoolSize(Serverless 并发低,大连接池浪费资源)。Serverless + MongoDB 的连接管理是运维痛点之一。
连接池工作机制:
graph LR
App1[请求 1] -->|借用| Pool[(连接池<br/>min=5 max=50)]
App2[请求 2] -->|借用| Pool
App3[请求 3] -->|等待| Queue[等待队列<br/>waitQueueTimeout]
Pool -->|连接 1| DB1[mongod]
Pool -->|连接 2| DB2[mongod]
Pool -->|连接 N| DB3[mongod]
App1 -.->|归还| Pool
App3 -.->|获取连接| Pool
style Pool fill:#d4edda
style Queue fill:#fff3cd
关键连接参数解读:
| 参数 | 默认值 | 说明 | 生产建议 |
|---|---|---|---|
| maxPoolSize | 100 | 最大连接数 | 50(按并发调整) |
| minPoolSize | 0 | 最小空闲连接 | 5(预热连接) |
| serverSelectionTimeoutMS | 30000 | 选服务器超时 | 5000(快速失败) |
| socketTimeoutMS | 0 | Socket 超时 | 45000(防僵尸连接) |
| maxIdleTimeMS | 0 | 空闲连接超时 | 30000(回收连接) |
连接参数的调优经验:连接参数没有万能值,需要根据实际场景调优——1. maxPoolSize:公式 = (每个连接内存约 1MB) + (并发请求数 × 平均查询时间)。4 核 8GB 服务器建议 50-100;2. serverSelectionTimeoutMS:设为 5000(5 秒)而非默认 30 秒——数据库不可用时 30 秒才报错太慢,5 秒快速失败并触发告警;3. heartbeatFrequencyMS:设为 10000(10 秒),更快检测到主节点切换(默认 10 秒已合理);4. retryWrites: true:自动重试一次写入操作(网络瞬断时自动恢复,对业务透明);5. 监控指标:连接池使用率(active/max)、等待队列长度、平均借用时间。使用率持续 > 80% 需要扩容(增大 maxPoolSize 或增加服务器)。
mongoose 连接的事件监控:mongoose 连接对象(mongoose.connection)发出多种事件用于监控——1. connected:成功连接(启动日志);2. error:连接错误(告警触发);3. disconnected:连接断开(告警触发,可能需要重连);4. reconnected:自动重连成功(记录日志,业务可能需要重新验证状态);5. close:连接关闭(优雅关闭时触发)。生产环境应监听所有事件并记录日志——连接断开是严重问题,可能影响所有数据库操作。监控方案:mongoose.connection.on('error', logger.error) + mongoose.connection.on('disconnected', alertOps)。 | autoIndex | true | 自动建索引 | 生产关闭 false |
使用场景:小型应用(< 1000 DAU)maxPoolSize=10-20;中型应用(1K-100K DAU)30-50;大型应用(> 100K DAU)50-100。超过 100 要谨慎,每条连接约占 1MB 内存。
(1) 连接函数
连接函数的设计考量:生产级连接函数不只是 mongoose.connect() 一行代码——它需要处理:1. 环境变量读取(MONGODB_URI 从 .env 获取,不硬编码);2. 连接选项配置(poolSize、timeout、autoIndex);3. 连接事件监听(connected、error、disconnected 日志);4. 进程退出时优雅关闭(SIGINT/SIGTERM 信号处理,mongoose.disconnect());5. 连接失败重试策略(指数退避重连)。连接函数通常放在独立模块 db.js 中,应用启动时最先调用。
// db.js
const mongoose = require('mongoose');
async function connectDB() {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
serverSelectionTimeoutMS: 5000,
maxPoolSize: 50, // 最大连接数
minPoolSize: 5, // 最小连接数
socketTimeoutMS: 45000,
autoIndex: true // 自动建索引(生产可关闭)
});
console.log(`✅ MongoDB connected: ${conn.connection.host}`);
return conn;
} catch (err) {
console.error('❌ Connection failed:', err.message);
process.exit(1);
}
}
module.exports = { connectDB };
graph TB
Client[客户端] -->|HTTP 请求| Router[Express Router]
Router -->|JWT 验证| AuthMW[认证中间件]
AuthMW -->|joi 验证| ValidateMW[验证中间件]
ValidateMW --> Controller[Controller<br/>业务逻辑]
Controller -->|CRUD| Model[mongoose Model]
Model -->|查询/写入| DB[(MongoDB)]
Controller -->|错误| ErrorMW[错误处理中间件]
ErrorMW -->|统一响应| Client
style Model fill:#d4edda
style Controller fill:#cce5ff
4. MVC 模式
MVC 分层原理:MVC 的核心价值是关注点分离——Model 只关心数据("是什么"),View 只关心展示("怎么看"),Controller 只关心协调("怎么做")。在 API 项目中,View 层退化为 JSON 序列化(res.json),但分层原则不变:改数据库只需改 Model,改路由只需改 Route,改业务逻辑只需改 Controller。不分层的代码把所有逻辑混在一起,改一个字段要搜索整个项目。
MVC 的依赖方向:严格 MVC 的依赖方向是单向的——Route → Controller → Model。Route 依赖 Controller(调用处理函数),Controller 依赖 Model(查询数据),但 Model 不依赖 Controller(数据层不知道谁在用它)。这种单向依赖使得每层可独立测试:Model 用单元测试、Controller 用集成测试、Route 用 HTTP 测试。
Service 层的引入时机:当 Controller 变"胖"时(超过 50 行或包含多个数据库操作),应将业务逻辑抽取到 Service 层。Service 的特征:1. 不依赖 Express(不知道 req/res 的存在);2. 接受纯数据参数,返回纯数据结果;3. 可被多个 Controller 复用(如 createOrder 同时被 Web API 和管理后台调用);4. 可独立单元测试(mock Model 即可,不需要 HTTP 请求)。引入 Service 的信号:Controller 中出现 if/else 嵌套、跨 Model 的事务逻辑、可复用的计算逻辑。
Model 层的"胖模型"设计哲学:mongoose 推崇"胖模型"——将业务逻辑封装在 Model 的静态方法、实例方法和中间件中。对比"瘦模型"(Model 只定义 Schema,所有逻辑在 Controller),胖模型的优势:1. 逻辑内聚(密码验证在 User 方法中,而非散落在多个 Controller);2. 代码复用(User.comparePassword() 可被登录和修改密码共用);3. 封装性(Controller 不知道密码如何验证,只需调用方法)。胖模型的边界:跨 Model 的逻辑不属于任何单个 Model,应放在 Service 中。
Controller 层的设计原则:Controller 是请求处理的协调者——它从 req 取数据、调用 Model/Service、返回 res。Controller 应保持"瘦"——1. 只做参数提取和类型转换(req.body → Service 参数);2. 只做一次 Model/Service 调用(复杂逻辑移到 Service);3. 统一错误处理(try/catch 包裹,next(err) 传递给错误中间件);4. 统一响应格式(res.json({success: true, data}))。瘦 Controller 的检验标准:每个 Controller 函数 < 30 行,没有业务逻辑(if/else 只有参数验证和错误分支)。
Express 中间件的执行顺序:中间件的注册顺序决定执行顺序——app.use(auth) → app.use(validate) → app.use(router) → app.use(errorHandler)。常见错误:1. 错误处理中间件放在路由前(导致所有请求都返回错误页面);2. 认证中间件放在不需要认证的路由后(部分路由被错误拦截);3. JSON 解析中间件(express.json())放在路由后(req.body 始终为 undefined)。中间件顺序的规则:通用中间件放前面(json、cors、helmet),路由特定中间件放路由内部(auth、validate)。
中间件的组合模式:Express 中间件支持灵活组合——1. 串行组合:authenticate → authorize → controller,前一步的输出是后一步的输入(authenticate 设置 req.user,authorize 检查 req.user.role);2. 条件组合:某些路由需要额外中间件(如 POST /products 需要 validate + authenticate,GET /products 不需要 authenticate),通过路由级中间件实现;3. 错误冒泡:任何中间件调用 next(err) 都会跳过后续所有普通中间件,直接进入错误处理中间件——这意味着验证失败、认证失败、数据库错误都统一在错误中间件中处理。理解中间件的组合模式,就能设计出灵活且可维护的请求处理流程。
中间件 vs Service 的职责边界:中间件处理横切关注点(认证、日志、错误处理),Service 处理业务逻辑(订单创建、评分计算)。混淆边界的标志——1. 中间件中有数据库查询(如认证中间件查用户是合理的,但验证中间件查商品就不合理);2. Service 中读取 req/res(Service 应只接受纯数据参数);3. Controller 只有 2-3 行(中间件做了太多事)。清晰的边界:中间件做"请求级"的事情(谁能访问、请求是否合法),Service 做"业务级"的事情(数据如何处理、规则如何应用)。
graph TB
subgraph "Route 层"
R1[GET /api/products]
R2[POST /api/products]
R3[GET /api/products/:sku]
end
subgraph "Controller 层"
C1[listProducts]
C2[createProduct]
C3[getProduct]
end
subgraph "Model 层"
M1[Product.find]
M2[Product.create]
M3[Product.findOne]
end
subgraph "MongoDB"
DB1[(products 集合)]
end
R1 --> C1 --> M1 --> DB1
R2 --> C2 --> M2 --> DB1
R3 --> C3 --> M3 --> DB1
style R1 fill:#cce5ff
style C1 fill:#d4edda
style M1 fill:#fff3cd
(1) 项目目录结构
目录结构的扩展策略:MVC 基础结构(models/controllers/routes/middlewares)适合中小项目。项目变大时需要扩展——1. 添加 validators/ 目录(joi Schema 与 Controller 分离,可复用);2. 添加 services/ 目录(业务逻辑从 Controller 抽取到 Service,Controller 只负责请求/响应转换);3. 添加 config/ 目录(数据库连接、环境变量集中管理);4. 添加 utils/ 目录(通用工具函数如 logger、jwt 工具)。扩展原则:每层只做一件事,单一职责。
models/ # 数据模型(mongoose Schema)
controllers/ # 业务逻辑
routes/ # 路由定义
middlewares/ # 中间件
services/ # 服务层(可选)
utils/ # 工具函数
(1) Model 层
Model 层设计原则:Model 层是数据的唯一真相源——所有数据定义、验证规则、查询方法都在 Model 中声明,Controller 和 Route 不应包含任何数据逻辑。具体原则:1. Schema 定义包含所有验证规则(Controller 不再重复验证);2. 索引在 Schema 中声明而非手动创建(mongoose 自动建索引);3. 虚拟字段、实例方法、静态方法定义在 Schema 上(业务逻辑内聚到 Model);4. select: false 隐藏敏感字段(passwordHash 默认不返回)。
Schema 的复用与继承:大型项目中多个 Model 可能共享相同的子结构——如 User 和 Admin 都有 address 字段。复用方式:1. 子 Schema 定义(const AddressSchema = new Schema({...}),然后在 UserSchema 和 AdminSchema 中都引用 AddressSchema);2. Schema.add() 动态添加字段;3. discriminators 继承(基类 Schema + 子类扩展字段,如 Product 基类 + Book/Product 子类)。子 Schema 是最常用的复用模式——独立的 Schema 可以有自己的验证规则和中间件。
索引声明策略:mongoose 支持三种索引声明方式——1. 字段级索引({sku: {type: String, index: true, unique: true}},简单直观适合单字段索引);2. Schema 级复合索引(schema.index({category: 1, price: -1}),适合多字段复合索引);3. 文本索引(schema.index({title: 'text', content: 'text'}),全文搜索专用)。生产环境注意:autoIndex 设 false(避免每次启动都建索引,索引创建应手动执行或用迁移脚本),开发环境设 true 方便自动同步。
// models/Product.js
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema({
sku: { type: String, required: true, unique: true, index: true },
title: { type: String, required: true },
price: { type: mongoose.Schema.Types.Decimal128, required: true },
category: { type: String, required: true, index: true },
stock: { type: Number, default: 0 }
}, { timestamps: true });
module.exports = mongoose.model('Product', ProductSchema);
(2) Controller 层
Controller 层设计原则:Controller 是请求处理的协调者——它从 req 提取参数、调用 Model 查询数据、用 res 返回响应。Controller 应该"薄"而非"胖"——1. 不包含业务逻辑(业务逻辑在 Model 的方法或 Service 层);2. 不直接操作数据库(通过 Model 方法间接操作);3. 不做数据转换(用 res.json 直接返回 lean() 结果);4. 错误交给 next(err) 传递给错误处理中间件(不在 Controller 中 try-catch 并直接 res.status)。
// controllers/productController.js
const Product = require('../models/Product');
exports.listProducts = async (req, res) => {
const { page = 1, limit = 20, category } = req.query;
const query = { isActive: true };
if (category) query.category = category;
const products = await Product.find(query)
.select('sku title price thumbnail')
.limit(limit * 1)
.skip((page - 1) * limit)
.lean();
res.json({ data: products, page, limit });
};
exports.getProduct = async (req, res) => {
const product = await Product.findOne({ sku: req.params.sku })
.select('-__v')
.lean();
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.json(product);
};
exports.createProduct = async (req, res) => {
const product = await Product.create(req.body);
res.status(201).json(product);
};
(3) Route 层
Route 层设计原则:Route 层只做 URL → Controller 的映射——不包含任何逻辑。但可以在映射中插入中间件——router.post('/', authenticate, authorize('admin'), validate(createSchema), ctrl.create) 这一行表达了完整的请求处理链:认证→授权→验证→业务处理。Route 层的职责:1. 定义 URL 模式(RESTful 风格);2. 映射 HTTP 方法到 Controller 方法;3. 挂载中间件(认证、授权、验证);4. 不包含任何数据处理逻辑。
Route 的模块化组织:大型项目的路由需要模块化——1. 按资源拆分文件(routes/products.js、routes/orders.js、routes/users.js),每个文件只定义一类资源的路由;2. 用 Express Router 创建模块化路由(const router = express.Router()),app.use('/api/products', productRouter) 挂载;3. 嵌套路由表达资源从属关系(/api/posts/:postId/comments 由 comments 路由处理);4. 路由版本化(app.use('/api/v1', v1Router)、app.use('/api/v2', v2Router))。模块化路由让团队协作更高效——每人负责一个路由文件,互不冲突。
中间件的组合策略:Express 中间件可以组合实现复杂的请求处理链——1. 全局中间件(app.use 层级):express.json()、cors()、helmet()、morgan()(日志)、rateLimit()(限流);2. 路由组中间件(router.use 层级):authenticate(需要登录的路由组)、authorize('admin')(管理员路由组);3. 单路由中间件(router.get 参数层级):validate(schema)(特定接口的验证逻辑)。中间件组合的威力——一个 router.post('/', auth, validate, ctrl.create) 就实现了"认证+验证+业务"三重保障,无需在 Controller 中重复这些逻辑。
// routes/products.js
const express = require('express');
const router = express.Router();
const ctrl = require('../controllers/productController');
router.get('/', ctrl.listProducts);
router.get('/:sku', ctrl.getProduct);
router.post('/', ctrl.createProduct);
module.exports = router;
5. 错误处理中间件
中间件链与错误传播:Express 中间件按注册顺序形成链式调用——next() 传递控制权给下一个中间件,next(err) 跳过所有普通中间件直接进入错误处理中间件。这种机制让错误处理集中在一处,避免每个路由都写 try-catch。但要注意:async 函数中的异常不会自动触发 next(err),必须用 try-catch 包裹或 express-async-errors 自动处理。
错误响应的信息安全:生产环境永远不要向客户端暴露原始错误——可能泄露数据库连接串、文件路径、堆栈信息。错误处理中间件的转换规则:ValidationError → 400(返回字段级错误详情),CastError → 400(返回"无效ID格式"),E11000 → 409(返回重复字段名),其他错误 → 500(只返回"内部错误",详情写日志)。
express-async-errors 的必要性:Express 4.x 的中间件不支持 async 函数——如果 async 中间件内抛异常,Express 不会捕获,请求会挂起直到超时。解决方案:1. 手动 try-catch + next(err)(代码冗余);2. express-async-errors 包(一行 require 自动包装所有路由处理函数);3. Express 5.x 原生支持 async 中间件。生产环境强烈推荐方案 2,只需在 app.js 顶部 require('express-async-errors') 即可,无需修改任何现有代码。
日志策略:错误处理中间件不仅要返回响应,还要记录日志——但日志详细程度应区分环境:开发环境输出完整堆栈(便于调试),生产环境只输出错误类型和请求 ID(避免敏感信息写入日志文件)。推荐用 winston 或 pino 替代 console.error——它们支持日志分级(error/warn/info/debug)、日志轮转、结构化输出(JSON 格式便于 ELK 检索)。
// middlewares/errorHandler.js
const errorHandler = (err, req, res, next) => {
console.error(err);
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'ValidationError',
details: Object.fromEntries(
Object.entries(err.errors).map(([k, v]) => [k, v.message])
)
});
}
if (err.code === 11000) {
return res.status(409).json({ error: 'Duplicate key', field: err.keyValue });
}
if (err.name === 'CastError') {
return res.status(400).json({ error: 'Invalid ID format' });
}
res.status(err.status || 500).json({
error: err.message || 'Internal Server Error'
});
};
module.exports = errorHandler;
6. 完整应用结构
连接池管理深度解析:mongoose 连接池是 MongoDB Node.js 驱动的核心机制——maxPoolSize 决定最大并发数据库操作数,minPoolSize 预热空闲连接避免冷启动延迟。连接池耗尽时新请求进入等待队列(waitQueueTimeoutMS 控制超时),超时后报错。生产调优:1. maxPoolSize = 应用并发数 × 1.5(留余量);2. minPoolSize = maxPoolSize × 0.1(预热);3. serverSelectionTimeoutMS 设 5000(快速失败而非 30 秒等待);4. autoIndex 生产关闭 false(索引创建阻塞连接)。
连接池耗尽的诊断:当应用报"Mongoose: connection timeout"时,可能的原因——1. 连接池太小(maxPoolSize < 实际并发数,调大池子);2. 慢查询占用连接(某查询执行 10 秒不释放,用 db.currentOp() 找到慢查询 kill 掉);3. 连接泄漏(代码中 find() 不加 await,游标不关闭);4. MongoDB 服务端负载过高(检查 CPU/内存/磁盘 I/O)。诊断步骤:1. 检查 mongoose.connection.readyState(0=断开、1=连接、2=连接中、3=断开中);2. 查看 db.serverStatus().connections 检查活跃连接数;3. 用 APM 工具(New Relic/Datadog)追踪数据库查询耗时。
连接池与 Serverless 的冲突:Serverless(AWS Lambda/Cloud Functions)环境下,每个函数实例创建独立的连接池——如果 100 个并发请求启动 100 个 Lambda,每个 Lambda 有 5 个连接,总共 500 个连接冲击 MongoDB。解决方案:1. 大幅减小 maxPoolSize(Lambda 场景建议 5-10);2. 使用 MongoDB Atlas 的 connection pooling 代理;3. 在 Lambda handler 外部初始化 mongoose.connect(复用连接池,减少冷启动时的连接数);4. 考虑用 HTTP API 替代直连(如 Data API/Realm Web SDK)。
连接事件监控:mongoose 连接对象发出多个事件可用于监控——1. mongoose.connection.on('connected', ...):成功连接到 MongoDB(记录日志);2. mongoose.connection.on('error', ...):连接错误(告警+重试);3. mongoose.connection.on('disconnected', ...):连接断开(可能是网络问题或 MongoDB 重启,自动重连机制会尝试恢复);4. mongoose.connection.on('reconnected', ...):重新连接成功(记录日志,业务可能已中断需恢复);5. mongoose.connection.on('close', ...):连接关闭(应用关闭或 mongoose.disconnect())。生产环境必须监听这些事件,否则连接静默断开会导致"所有数据库查询超时"的线上事故。
优雅关闭的最佳实践:应用关闭时必须优雅断开 MongoDB 连接——1. 监听 SIGINT/SIGTERM 信号(Ctrl+C 或 Kubernetes pod 终止信号);2. 停止接收新请求(server.close());3. 等待进行中的请求完成(连接池中的活跃查询);4. 断开 mongoose 连接(mongoose.disconnect());5. 进程退出(process.exit(0))。不优雅关闭的后果:进行中的数据库操作被中断,可能导致数据不一致(如写了一半的文档)。Kubernetes 给 pod 30 秒优雅关闭时间,应用必须在 30 秒内完成上述步骤。
sequenceDiagram
participant Main as app.js
participant Dotenv as dotenv
participant DB as connectDB
participant Express as Express App
participant Listen as app.listen
Main->>Dotenv: 加载 .env
Dotenv-->>Main: 环境变量就绪
Main->>Express: 创建 app + 中间件
Main->>Express: 注册路由
Main->>Express: 注册错误处理
Main->>DB: await connectDB()
DB-->>Main: ✅ MongoDB connected
Main->>Listen: app.listen(PORT)
Listen-->>Main: 🚀 Server running
中间件注册顺序:Express 按注册顺序执行中间件,顺序至关重要:json解析 → 日志 → 路由 → 404处理 → 错误处理。错误处理必须放最后。
404 中间件的实现细节:404 中间件是一个没有路径参数的普通中间件——放在所有路由之后,当前面的路由都没有匹配时执行。实现:app.use((req, res) => res.status(404).json({error: 'Not Found'}))。注意:404 中间件不是错误中间件(4 个参数),而是普通中间件(3 个参数),它不需要 next() 因为没有后续中间件。常见错误:把 404 中间件放在路由之前(所有请求都返回 404),或忘记写 404 中间件(未匹配路由由 Express 默认处理,返回 HTML 格式的 Cannot GET /xxx,而非 JSON)。
| 顺序 | 中间件 | 作用 |
|---|---|---|
| 1 | express.json() | 解析请求体 |
| 2 | cors() | 跨域支持 |
| 3 | 日志中间件 | 请求记录 |
| 4 | 路由 | 业务处理 |
| 5 | 404 中间件 | 未匹配路由 |
| 6 | 错误处理 | 统一错误响应 |
环境配置管理:Express + mongoose 项目需要管理多个环境(development/test/production)的配置——1. .env 文件存储本地开发配置(数据库 URL、端口、密钥),不提交到 Git;2. .env.example 提交到 Git 作为模板;3. production 配置通过环境变量注入(Docker/K8s/云平台),不使用 .env 文件;4. config/ 目录按环境加载不同配置(config/development.js、config/production.js)。关键原则:密钥和密码永远不硬编码、不提交 Git。
优雅关闭(Graceful Shutdown):生产环境的 Express 应用必须支持优雅关闭——收到 SIGTERM/SIGINT 信号后:1. 停止接受新请求(server.close());2. 等待正在处理的请求完成(设置超时如 10 秒);3. 关闭 mongoose 连接(mongoose.disconnect());4. 退出进程。没有优雅关闭的后果:正在处理的数据库操作被强制中断,可能导致数据不一致。Kubernetes/Docker 的滚动更新依赖优雅关闭——旧 Pod 必须在超时前完成所有请求。
健康检查端点:生产环境必须提供 /health 端点——用于负载均衡器判断应用是否健康、Kubernetes liveness/readiness 探针。健康检查应包含:1. HTTP 200 响应(应用进程存活);2. mongoose.connection.readyState === 1(数据库连接正常);3. 可选检查:Redis 连接、磁盘空间、内存使用率。简单实现:app.get('/health', (req, res) => { if (mongoose.connection.readyState === 1) res.json({status: 'ok'}); else res.status(503).json({status: 'db disconnected'}); })。Kubernetes 配置:livenessProbe 每 10 秒检查一次,连续 3 次失败重启 Pod;readinessProbe 每 5 秒检查一次,连续 2 次失败从 Service 移除 Pod。
安全防御的纵深体系:Express 应用的安全不是单一措施而是纵深防御——1. Helmet 中间件(设置 12 个安全 HTTP 头:X-Content-Type-Options、X-Frame-Options、CSP 等);2. CORS 策略(cors() 白名单限制允许的 Origin,不设 Access-Control-Allow-Origin: *);3. 速率限制(express-rate-limit 防暴力破解和 DDoS);4. 输入验证(joi/express-validator 防注入攻击);5. 认证授权(JWT + RBAC 防越权操作);6. HTTPS(TLS 加密传输,防中间人攻击)。每层独立防护,任何一层被突破不影响其他层的防御效果。
健康检查端点:生产应用必须提供 /health 和 /ready 端点——/health 检查进程是否存活(简单的 200 OK),/ready 检查是否准备好接受请求(包括 MongoDB 连接是否正常)。Kubernetes 用这两个端点做 liveness probe 和 readiness probe。/ready 的实现:try { await mongoose.connection.db.admin().ping() } catch { return res.status(503).json({ready: false}) }。如果 MongoDB 不可达,/ready 返回 503,K8s 暂停向该 Pod 发送流量但不重启它(与 liveness 不同)。
健康检查的进阶设计:生产级健康检查不只是 ping 数据库——1. 依赖检查:依次验证 MongoDB(ping)、Redis(ping)、外部 API(HEAD 请求),任何一个不可达标记为 not ready;2. 启动延迟:应用启动后不要立即标记 ready(mongoose 连接可能还在建立中),等 connection.on('connected') 后再标记;3. 超时控制:健康检查本身不能阻塞(设 5s 超时,超时视为 not ready);4. 降级模式:如果 MongoDB 不可达但 Redis 可达,可返回 degraded 状态(提供缓存数据而非完全不可用)。健康检查是运维和开发的桥梁——开发提供端点,运维配置探针,SRE 设置告警。
// app.js
require('dotenv').config();
const express = require('express');
const { connectDB } = require('./db');
const productRoutes = require('./routes/products');
const errorHandler = require('./middlewares/errorHandler');
const app = express();
app.use(express.json());
app.get('/healthz', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
app.use('/api/products', productRoutes);
app.use(errorHandler);
(async () => {
await connectDB();
app.listen(process.env.PORT || 3000);
})();
7. dotenv 环境变量
为什么需要环境变量? 硬编码的数据库连接串、JWT 密钥、端口号散落在代码中——这意味着:① 代码泄露即密钥泄露;② 换环境(dev/staging/prod)要改代码;③ 协作时每人配置不同。dotenv 的解决方案是:从 .env 文件加载变量到 process.env,代码只读环境变量,不同环境用不同 .env 文件。
环境变量的安全分级:环境变量按敏感度分三级——1. 公开配置(PORT、NODE_ENV、LOG_LEVEL):可提交到 Git,不敏感;2. 内部配置(MONGODB_URI、REDIS_URL、API_BASE_URL):不提交到 Git,团队内部共享,通过 .env 文件或 CI/CD 注入;3. 密钥凭据(JWT_SECRET、AWS_ACCESS_KEY、DATABASE_PASSWORD):最高敏感度,不提交到 Git、不写在 .env 文件中,通过密钥管理服务(AWS Secrets Manager/HashiCorp Vault)注入。分级管理避免"所有配置都在 .env 中,一旦 .env 泄露全部沦陷"的风险。
环境变量管理的最佳实践:
| 实践 | 说明 |
|---|---|
| .env 不入库 | 加入 .gitignore,防止泄露 |
| .env.example 入库 | 提供模板,不含真实值 |
| 生产用密钥管理 | AWS Secrets Manager / HashiCorp Vault |
| 验证必要变量 | 启动时检查 MONGODB_URI 等是否存在 |
| 区分环境 | NODE_ENV=development/production |
启动时的环境变量验证:应用启动时应验证所有必要的环境变量是否存在——如果 MONGODB_URI 未设置,应用启动后所有数据库操作都会失败,不如在启动时就报错退出。验证方案:1. 简单验证(if (!process.env.MONGODB_URI) throw new Error('MONGODB_URI is required'));2. 用 dotenv-safe(自动比较 .env 和 .env.example,缺少变量就报错);3. 用 joi 验证(定义 configSchema,验证 process.env 的值是否合法,如 PORT 必须是数字)。生产环境推荐方案 3——不仅检查存在性还检查合法性(PORT 必须是 1024-65535 的数字,NODE_ENV 必须是 development/test/production 之一)。
配置层级:环境变量 > .env 文件 > 默认值。代码中应该这样写:process.env.PORT || 3000——环境变量优先,没有则用默认值。
# .env
MONGODB_URI=mongodb://localhost:27017/shopdb
PORT=3000
NODE_ENV=development
JWT_SECRET=your-secret-key
// config.js
require('dotenv').config();
module.exports = {
port: parseInt(process.env.PORT) || 3000,
mongoUri: process.env.MONGODB_URI,
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV || 'development'
};
8. 实战:完整 CRUD API
RESTful 路由设计原则:资源名用复数名词(products 非 product),嵌套资源用层级路径(/products/:id/reviews),HTTP 方法表达操作语义(GET 读 / POST 写 / DELETE 删)。认证路由用 /auth 前缀,业务路由用 /api 前缀。
路由设计的常见争议:RESTful 路由设计有几个常见争议——1. 复数 vs 单数:/products 还是 /product?业界惯例用复数(表示集合),但 GitHub API 用单数。统一比选择哪个更重要;2. 嵌套深度:/products/:id/reviews/:reviewId 还是 /reviews/:reviewId?建议最多 2 层嵌套,超过 2 层用顶级资源 + 过滤参数(如 /reviews?productId=xxx);3. Action 端点:/users/:id/activate 是 RESTful 吗?严格说不是,但实际项目中常见(比 PUT /users/:id {status: 'active'} 更直观)。REST 是指导不是教条,可读性和实用性优先。
API 安全要点:
| 安全措施 | 实现方式 |
|---|---|
| 认证 | JWT Bearer Token |
| 授权 | RBAC 角色检查(admin/customer) |
| 输入验证 | joi Schema 验证 |
| 速率限制 | express-rate-limit |
| CORS | 白名单域名 |
| 请求体限制 | express.json({ limit: '1mb' }) |
安全的纵深防御策略:API 安全不是单一措施,而是多层防护——1. 网络层(HTTPS 加密、WAF 防火墙、IP 白名单);2. 应用层(认证 + 授权 + 输入验证 + 速率限制);3. 数据层(mongoose Schema 验证 + $jsonSchema 兜底 + select: false 隐藏敏感字段);4. 运维层(日志审计 + 异常检测 + 自动封禁)。每层独立防护,任何一层被突破仍有其他层保护。常见错误是只依赖某一层(如只做认证不做授权,或只做 Schema 验证不做输入验证)。
速率限制的设计策略:express-rate-limit 的限流策略需要区分场景——1. 全局限流(所有 API 共享,如每分钟 100 次,防 DDoS);2. 认证接口严限(登录/注册每 IP 每分钟 5 次,防暴力破解);3. 写入接口中限(创建/更新每用户每分钟 30 次,防垃圾数据);4. 读取接口宽限(列表/详情每用户每分钟 200 次,正常使用不阻断)。限流粒度按 IP + 用户 ID 双维度——同一 IP 多用户不互相影响,同一用户换 IP 仍受限。
CORS 配置的安全原则:CORS(跨域资源共享)配置决定了哪些前端域名可以调用 API——生产环境绝不使用 Access-Control-Allow-Origin: *。正确的配置:1. 白名单域名列表(如 ['https://shop.example.com', 'https://admin.example.com']);2. 允许的 HTTP 方法(GET/POST/PUT/DELETE,不含 OPTIONS 以外的特殊方法);3. 允许的请求头(Content-Type, Authorization);4. 凭证支持(credentials: true 时必须指定具体域名,不能用通配符)。开发环境可以用 *,但必须通过 NODE_ENV 区分。
// routes/reviews.js
const express = require('express');
const router = express.Router();
const Review = require('../models/Review');
const { authenticate } = require('../middlewares/auth');
router.get('/products/:productId/reviews', async (req, res) => {
const reviews = await Review.find({ productId: req.params.productId })
.populate('userId', 'username avatar')
.sort({ createdAt: -1 })
.limit(20)
.lean();
res.json(reviews);
});
router.post('/products/:productId/reviews', authenticate, async (req, res) => {
const review = await Review.create({
productId: req.params.productId,
userId: req.user._id,
content: req.body.content,
rating: req.body.rating
});
res.status(201).json(review);
});
module.exports = router;
路由级别的中间件挂载:Express 中间件可以挂载到不同层级——1. 应用级(app.use(cors())),所有路由生效;2. 路由级(router.use(authenticate)),该路由下的所有端点生效;3. 端点级(router.post('/', authenticate, validate, ctrl.create)),仅该端点生效。粒度越细,控制越精确,但代码重复越多。推荐策略:通用中间件(cors/json/logging)放应用级,认证放路由级,验证和授权放端点级。
Controller 的异步错误处理:Express 4.x 不会自动捕获 async 函数中的异常——如果 Controller 的 async 函数内 await 抛错,Express 不会调用 next(err),请求会挂起。三种解决方案:1. express-async-errors(一行 require 全局解决,最推荐);2. 手动包裹 try-catch + next(err)(代码冗余但显式);3. 高阶函数 wrapAsync(fn) 自动包裹(灵活但需每个路由手动包装)。方案 1 是零侵入的,安装后所有 async 路由自动具备错误传播能力。
查询构建器的链式调用:mongoose 的查询构建器支持链式调用——Model.find(query).select(fields).populate(ref).sort(order).skip(n).limit(m).lean()。链式调用的顺序不影响查询结果(mongoose 内部会优化),但可读性最佳的顺序是:find → select → populate → sort → skip → limit → lean。这个顺序符合"先定义查什么,再决定怎么展示"的思维逻辑。lean() 永远放最后——因为它将查询结果从 mongoose Document 转为纯 JS 对象,之后无法再链式调用 Document 方法。
批量操作的性能:当需要创建/更新/删除多条记录时,批量操作比逐条操作快 10-100 倍——1. Model.insertMany([...]) 一次网络往返插入 N 条,比 N 次 Model.create() 快 N 倍;2. Model.updateMany(filter, update) 一次更新所有匹配文档,比逐条 updateOne 快得多;3. Model.deleteMany(filter) 批量删除。但批量操作的限制:1. 不触发 pre/post save 中间件(只有 validate 中间件在 insertMany 中触发);2. 不返回完整的文档对象(只返回写确认);3. 单次操作的数据量受 BSON 16MB 限制。
▶ 示例 1:Express + mongoose 基础连接与路由
// === 1. 最小 Express + mongoose 应用 ===
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.json());
// mongoose 连接
mongoose.connect(process.env.MONGODB_URI, {
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000
}).then(() => console.log('✅ MongoDB connected'))
.catch(err => { console.error('❌ Connection failed:', err.message); process.exit(1); });
// 最简单 CRUD
app.get('/api/products', async (req, res) => {
const products = await mongoose.model('Product').find().lean();
res.json({ data: products });
});
app.post('/api/products', async (req, res) => {
const product = await mongoose.model('Product').create(req.body);
res.status(201).json({ data: product });
});
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
app.listen(3000, () => console.log('🚀 Server running on port 3000'));
输出:最小可运行的 Express + mongoose 应用,3 个文件(app.js + .env + package.json)即可启动。
▶ 示例 2:Express + mongoose 完整电商 API 架构
// === 完整项目结构 ===
// shophub-api/
// ├── src/
// │ ├── app.js # Express 应用入口
// │ ├── config/
// │ │ ├── db.js # mongoose 连接
// │ │ └── index.js # 环境配置
// │ ├── models/
// │ │ ├── User.js
// │ │ ├── Product.js
// │ │ └── Order.js
// │ ├── controllers/
// │ │ ├── authController.js
// │ │ ├── productController.js
// │ │ └── orderController.js
// │ ├── routes/
// │ │ ├── auth.js
// │ │ ├── products.js
// │ │ └── orders.js
// │ ├── middlewares/
// │ │ ├── auth.js # JWT 验证
// │ │ ├── errorHandler.js
// │ │ └── validate.js # joi 验证
// │ └── utils/
// │ └── logger.js
// ├── .env
// └── package.json
// === 1. db.js - 连接配置 ===
const mongoose = require('mongoose');
async function connectDB() {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
serverSelectionTimeoutMS: 5000,
maxPoolSize: 50,
minPoolSize: 5,
socketTimeoutMS: 45000,
autoIndex: process.env.NODE_ENV !== 'production' // 生产关闭 autoIndex
});
console.log(`✅ MongoDB connected: ${conn.connection.host}`);
return conn;
}
module.exports = { connectDB };
// === 2. middlewares/auth.js - JWT 验证 ===
const jwt = require('jsonwebtoken');
exports.authenticate = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'No token' });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
};
exports.authorize = (...roles) => (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
// === 3. controllers/productController.js ===
const Product = require('../models/Product');
exports.list = async (req, res, next) => {
try {
const { page = 1, limit = 20, category, search, sort = 'createdAt', order = 'desc' } = req.query;
const query = { isActive: true };
if (category) query.category = category;
if (search) query.title = new RegExp(search, 'i');
const [products, total] = await Promise.all([
Product.find(query)
.select('sku title price thumbnail rating')
.sort({ [sort]: order === 'desc' ? -1 : 1 })
.skip((page - 1) * limit)
.limit(+limit)
.lean(),
Product.countDocuments(query)
]);
res.json({
success: true,
data: products,
meta: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) }
});
} catch (err) { next(err); }
};
exports.get = async (req, res, next) => {
try {
const product = await Product.findOne({ sku: req.params.sku }).lean();
if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
res.json({ success: true, data: product });
} catch (err) { next(err); }
};
exports.create = async (req, res, next) => {
try {
const product = await Product.create(req.body);
res.status(201).json({ success: true, data: product });
} catch (err) { next(err); }
};
// === 4. routes/products.js ===
const router = require('express').Router();
const ctrl = require('../controllers/productController');
const { authenticate, authorize } = require('../middlewares/auth');
router.get('/', ctrl.list);
router.get('/:sku', ctrl.get);
router.post('/', authenticate, authorize('admin'), ctrl.create);
module.exports = router;
// === 5. app.js - 主应用 ===
require('dotenv').config();
const express = require('express');
const { connectDB } = require('./config/db');
const app = express();
app.use(express.json({ limit: '1mb' }));
// 健康检查
app.get('/healthz', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
// 路由
app.use('/api/products', require('./routes/products'));
app.use('/api/orders', require('./routes/orders'));
app.use('/api/auth', require('./routes/auth'));
// 错误处理
app.use(require('./middlewares/errorHandler'));
(async () => {
await connectDB();
app.listen(process.env.PORT || 3000, () => {
console.log(`🚀 Server running on port ${process.env.PORT || 3000}`);
});
})();
输出:完整 MVC 架构,支持 JWT 认证、RBAC 权限、错误处理、健康检查。
MVC 架构的演进路径:项目从小到大的架构演进分三个阶段——1. 单文件阶段(MVP):所有逻辑在 app.js 中,适合 Demo 和原型(< 100 行代码);2. 分层阶段(生产):Model/Controller/Route 分文件 + 中间件 + 配置,适合正式项目(100-1000 行);3. 模块化阶段(扩展):按业务域分模块(user/order/product 各自含 Model+Controller+Route),模块间通过 Service 层通信,适合大型项目(1000+ 行)。演进的核心原则是"只在痛点出现时才引入复杂度"——不要在 MVP 阶段就设计模块化架构,也不要在 1000+ 行时还用单文件。
生产环境的启动检查清单:应用上线前必须检查——1. 环境变量:所有必要变量已设置且合法(MONGODB_URI、JWT_SECRET、NODE_ENV=production);2. 数据库连接:连接池配置合理(maxPoolSize 根据并发量设置)、超时时间设置(serverSelectionTimeoutMS: 5000);3. 安全中间件:helmet(安全头)、cors(跨域白名单)、express-rate-limit(限流)、mongo-sanitize(防注入);4. 日志:winston/morgan 配置正确,错误日志写入文件而非只输出控制台;5. 优雅关闭:SIGTERM/SIGINT 信号处理,关闭数据库连接后再退出进程;6. 健康检查:/health 端点可被负载均衡器探测。6 项检查通过后方可上线。
▶ 示例 3:Express 中间件链 + 统一错误处理实战
中间件是 Express 的核心机制——请求/响应对象在中间件链中依次传递,每个中间件处理一个关注点。本示例实现一个生产级的中间件架构:请求日志 → 安全头 → CORS → 限流 → 认证 → 业务处理 → 统一错误处理 → 响应格式化。
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const morgan = require('morgan');
const mongoose = require('mongoose');
const app = express();
// === 1. 基础中间件(所有请求经过)===
app.use(helmet()); // 安全头:X-Content-Type-Options, X-Frame-Options 等
app.use(cors({
origin: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true
}));
app.use(express.json({ limit: '1mb' })); // 请求体大小限制
app.use(morgan('combined', { // 访问日志
skip: (req) => req.path === '/health' // 健康检查不记日志
}));
// === 2. 限流中间件 ===
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟窗口
max: 100, // 每个 IP 最多 100 次
standardHeaders: true,
message: { error: '请求过于频繁,请稍后再试' }
});
app.use('/api/', apiLimiter);
// === 3. 认证中间件(可选认证,不影响公开接口)===
function optionalAuth(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return next(); // 无 token 继续执行,req.user 为 undefined
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
} catch {
// token 无效也继续,让具体路由决定是否要求认证
}
next();
}
app.use(optionalAuth);
// === 4. 统一响应格式化中间件 ===
function wrapAsync(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
res.success = function(data, meta = {}) {
return this.json({ success: true, data, ...meta });
};
res.paginate = function(data, total, page, limit) {
return this.json({
success: true,
data,
pagination: { total, page, limit, totalPages: Math.ceil(total / limit) }
});
};
// === 5. 统一错误处理中间件(4 个参数)===
app.use((err, req, res, next) => {
// mongoose 错误分类
if (err.name === 'ValidationError') {
const messages = Object.values(err.errors).map(e => e.message);
return res.status(400).json({ success: false, error: '验证失败', details: messages });
}
if (err.name === 'CastError') {
return res.status(400).json({ success: false, error: `无效的 ${err.path}: ${err.value}` });
}
if (err.code === 11000) {
const field = Object.keys(err.keyPattern)[0];
return res.status(409).json({ success: false, error: `${field} 已存在` });
}
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, error: '认证失败' });
}
// 未知错误
console.error('未处理错误:', err);
const message = process.env.NODE_ENV === 'production'
? '服务器内部错误'
: err.message;
res.status(err.status || 500).json({ success: false, error: message });
});
// === 6. 优雅关闭 ===
process.on('SIGTERM', async () => {
console.log('收到 SIGTERM,开始优雅关闭...');
server.close(() => console.log('HTTP 服务器已关闭'));
await mongoose.disconnect();
console.log('数据库连接已关闭');
process.exit(0);
});
const server = app.listen(process.env.PORT || 3000);
输出:完整的 Express 中间件链——请求依次经过安全头、CORS、限流、认证,响应经过统一格式化,错误经过分类处理。优雅关闭确保 SIGTERM 信号时先关 HTTP 再关数据库。
中间件架构的设计原则:1. 顺序很重要——helmet → cors → body-parser → 认证 → 业务,依赖链必须正确;2. 错误处理放最后——4 个参数的中间件只在 next(err) 调用后执行;3. wrapAsync 模式——所有 async 路由用 wrapAsync 包裹,避免忘记 catch 导致未处理 Promise 拒绝;4. 响应格式统一——所有成功响应用 res.success/res.paginate,所有错误响应由错误中间件统一格式化;5. 限流分层——全局限流 + 路由级限流(如 /api/auth/login 更严格)。
❓ 常见问题
📖 小节
- Express + mongoose 完整集成
- MVC 模式:Model / View / Controller
- RESTful 路由设计
- 错误处理中间件
- dotenv 环境变量管理
- 健康检查端点 /healthz
📝 作业
- 基础题(⭐):搭建 Express + mongoose 项目骨架,含 connectDB + 基本路由。
- 基础题(⭐):实现错误处理中间件(区分 ValidationError / CastError / 11000)。
- 进阶题(⭐⭐):实现完整 CRUD API(products),含分页、筛选、投影。
- 进阶题(⭐⭐):用 dotenv 管理环境变量,区分 dev/prod 配置。
- 挑战题(⭐⭐⭐):完整评论系统 API(GET/POST/PUT/DELETE + 认证中间件 + 错误处理)。