MongoDB 和 Express 这两个东西单独拎出来看都不算难难的是把它们凑到一块跑通第一条完整链路数据能写进去、能查出来、接口能返回。我带过不少人入门卡住的位置出奇地一致——装完 MongoDB敲了mongod终端刷一堆日志然后窗口一关就没了装完 Express能响应一个 hello world但一接数据库就报一堆看不懂的错。这篇就把 MongoDB、Express 的安装和基本使用这条线从头到尾捋一遍重点放在那些文档里不写、但第一次动手一定会卡住的细节上。适合完全没碰过后端的人照着做也适合那种能跑但不知道为什么这么写的人回来补一遍底层逻辑。1. 这套组合的定位先搞清楚自己在装什么在装任何东西之前值得花两分钟想清楚这两块各自负责什么不然装完只会照着教程复制命令出了错完全不知道往哪个方向排查。1.1 文档数据库和表格数据库的思维切换MongoDB 是文档型数据库数据以 BSON二进制的 JSON形式存放在集合collection里一条记录就是一个文档document。这跟关系型数据库的表 行 列完全不是一回事。关系型数据库要求你先定义好表结构每一行的字段固定加个字段就得ALTER TABLEMongoDB 里同一个集合下的两个文档字段可以不一样嵌套对象、数组直接往里塞。这个差异带来的第一个实际影响是你在设计数据时优先考虑的是这个页面要一次性拿到什么而不是怎么消除冗余。比如做博客评论可以直接嵌在文章文档里一次查询就全拿到了而关系型数据库你会想着拆成两张表再 JOIN。反过来说如果评论数量会无限膨胀、需要独立分页那就该拆成独立集合用postId关联。这不是谁更好的问题是数据形态决定的选择。第二个影响是没有强制的 schema。听起来自由实际上是个陷阱。新手很容易在一个集合里塞进三种不同结构的文档两周后自己都记不清哪条数据有哪些字段。所以后面讲到 Mongoose 的时候我会坚持让大家写 Schema——不是因为 MongoDB 需要而是因为你的代码需要。1.2 Express 的薄到底薄在哪Express 的定位是极简的 Web 框架说人话就是它只帮你做了三件事——把 HTTP 请求解析成对象、按路径和请求方法分发到对应的处理函数、把处理函数的返回值打包成响应。除此之外路由分组、参数校验、鉴权、日志、错误处理全要你自己加或者找中间件。这种薄对学习是好事你能清楚看到每一个请求进来之后发生了什么对生产是坏事因为稍不注意就会写出一个几百行、什么都往里塞的app.js。我见过不少项目路由、业务逻辑、数据库操作全挤在一个文件里改一个字段要翻三处代码。所以从第一天起就该有分层意识哪怕只是把路由文件和入口文件拆开这么简单的一步。1.3 动手前必须确认的三个前置条件装之前先确认环境能省掉一半的玄学问题。Node.js 版本。Express 5 要求 Node 18 以上Mongoose 8 也要求 Node 16.20 以上。跑node -v看一眼低于 18 建议直接用版本管理工具切一个 LTS 版本别硬扛。端口占用情况。MongoDB 默认吃 27017Express 常用 3000。这两个端口在本机被别的软件占用的概率不低尤其是前后端一起开发的时候前端脚手架也爱抢 3000。提前用lsof -i :3000或netstat -ano | findstr :3000查一下。磁盘写入权限。MongoDB 需要往数据目录写文件Windows 装在C:\Program Files下时服务账户权限没配好会出现启动了但立刻退出的情况。这个后面会具体讲。提示如果你只是在本地做练习不要一上来就折腾云端托管数据库。本地跑通 CRUD理解连接、索引、查询这些东西之后再迁移到托管服务中间只差改一个连接串。2. MongoDB 本地安装从下载到服务自启的完整链路安装这一步最容易出问题的地方不是安装本身而是装完之后它到底有没有在后台老老实实跑着。很多人装完以为万事大吉写代码一连就报错回头才发现服务压根没启动。2.1 三个平台各自的安装路径差异Windows走 MSI 安装包最省事。安装向导里有一个关键勾选项Install MongoDB as a Service。勾上之后安装程序会自动把mongod注册成系统服务开机自启你不需要手动开一个终端窗口挂着。默认安装路径是C:\Program Files\MongoDB\Server\版本号\bin目录下你会看到mongod.exe服务端和mongosh.exe新版自带老版本需要单独装。安装向导里还有一个Run MongoDB as a Network Service User的选项本地开发选默认就行。macOS用 Homebrew 最干净brew tap mongodb/brew brew install mongodb-community7.0 brew services start mongodb-community7.0注意brew services start和直接运行mongod的区别前者是把它注册成 launchd 服务开机自启且有统一日志后者只是前台跑一个进程终端一关就结束。新手最常见的误会就是把这两者混为一谈。Linux有两种路径。喜欢用系统包管理器的可以配置官方软件源后用apt或dnf安装喜欢完全掌控的下载 tgz 解压到/usr/local/mongodb自己建配置文件和 systemd 单元。我更推荐后者给学习用途因为你能亲眼看到目录结构、数据文件在哪、日志在哪出问题好排查。不管哪个平台验证方式都一样mongod --version mongosh --version两条都能输出版本号说明二进制文件到位了。但二进制到位不等于服务在跑这是两码事。2.2 手写一份 mongod.conf 的意义MongoDB 的启动参数可以全写在命令行里但那意味着每次启动都要敲一长串而且没法做成服务。正经做法是写配置文件。Linux/macOS 下的配置长这样storage: dbPath: /usr/local/mongodb/data journal: enabled: true systemLog: destination: file path: /usr/local/mongodb/log/mongod.log logAppend: true net: port: 27017 bindIp: 127.0.0.1Windows 下安装向导会生成mongod.cfg结构基本一样只是路径换成 Windows 风格。几个字段值得单独说配置项作用常见误区storage.dbPath数据文件存放位置目录必须提前存在且有写权限MongoDB 不会自动创建net.bindIp监听地址写0.0.0.0会让局域网内所有机器都能连本地开发务必用127.0.0.1net.port监听端口改了端口所有连接串都要跟着改容易漏systemLog.path日志文件位置目录不存在时启动直接失败日志都没得看security.authorization是否开启鉴权初学阶段关着方便但只要绑定地址不是回环地址就一定要开storage.dbPath那个坑我踩过不止一次。MongoDB 启动时会检查这个目录不存在就报NonExistentPath: Data directory ... not found然后退出。这不是权限问题是目录根本不存在但报错信息看起来很像权限错很容易往错的方向查。养成习惯装完先把数据目录和日志目录手动mkdir -p建好。bindIp那条也值得强调。默认配置里常能看到bindIp: 127.0.0.1和bindIp: 0.0.0.0两种写法后者方便你在虚拟机、容器里从宿主机连但代价是这台机器暴露在网络上时任何人扫到 27017 端口都能直接连库。本地开发老老实实写127.0.0.1需要跨机器访问时再改用 SSH 端口转发之类的安全方式。2.3 服务注册与开机自启的实操Linuxsystemd下建一个单元文件/etc/systemd/system/mongod.service[Unit] DescriptionMongoDB Database Server Afternetwork.target [Service] Usermongodb Groupmongodb ExecStart/usr/local/mongodb/bin/mongod --config /usr/local/mongodb/etc/mongod.conf Restarton-failure LimitNOFILE64000 [Install] WantedBymulti-user.target然后sudo systemctl daemon-reload sudo systemctl enable --now mongod sudo systemctl status mongodRestarton-failure这一行很有用进程异常退出后自动拉起来省得你半夜发现服务挂了。LimitNOFILE是文件描述符上限MongoDB 在高并发下会占用大量句柄默认值偏小。macOS用brew servicesbrew services list brew services restart mongodb-community7.0brew services list能看到当前状态是started还是error。如果是error日志通常在/usr/local/var/log/mongodb/mongo.logIntel 机器或/opt/homebrew/var/log/mongodb/mongo.logApple Silicon。路径不同这一点很坑很多人照着 Intel 的路径去找日志结果文件不存在以为日志功能坏了。Windows下如果你在安装时没勾选服务选项也可以手动装sc.exe create MongoDB binPath \C:\Program Files\MongoDB\Server\7.0\bin\mongod.exe\ --config \C:\Program Files\MongoDB\Server\7.0\bin\mongod.cfg\ DisplayName MongoDB start auto注意binPath后面那个空格是必须的这是sc.exe的语法怪癖漏了就会报参数错误。2.4 怎么确认它是真的活了服务起来之后用mongosh连一下mongosh mongodb://127.0.0.1:27017看到提示符变成test就说明连上了。这时候敲一句db.runCommand({ ping: 1 })返回{ ok: 1 }才算真正连通。这比看服务状态更可靠因为服务正在运行和能接受连接是两回事——监听端口还没起来、配置解析到一半失败都可能出现状态显示 running 但连不上的情况。3. mongosh 里的第一课库、集合、文档的手感mongosh是 MongoDB 的交互式 shell语法是 JavaScript。别把它当成一个数据库客户端就完事了它本质上是一个 JS 运行环境你可以写循环、定义变量、调用函数。理解这一点后面写批量操作会顺手很多。3.1 惰性创建为什么 use 一个库 show dbs 却看不到这是新手最经典的一个困惑use blog show dbsshow dbs里没有blog。不是你操作错了是 MongoDB 的库和集合都是惰性创建的——执行use blog只是把当前上下文切过去真正的库要等你写入第一条数据才会创建。use blog db.posts.insertOne({ title: 第一篇, status: draft }) show dbs这次blog就出现了。集合posts同理db.createCollection(posts)可以不写第一次insertOne自动建。惰性创建的好处是不会产生一堆空库坏处是你不能靠库存在来判断初始化是否成功。实际项目里初始化脚本末尾一定要插入一条哨兵数据或者调用一次db.stats()才能真正确认结构建好了。3.2 四条 CRUD 命令的正确姿势基础的四条命令// 增 db.posts.insertOne({ title: Express 路由分层实践, slug: express-router-layers, tags: [node, express], status: draft, createdAt: new Date() }) // 查 db.posts.find({ status: draft }).sort({ createdAt: -1 }).limit(10) // 改 db.posts.updateOne( { slug: express-router-layers }, { $set: { status: published }, $currentDate: { updatedAt: true } } ) // 删 db.posts.deleteOne({ slug: express-router-layers })updateOne的第二个参数必须带更新操作符$set、$inc、$push等如果直接写{ status: published }MongoDB 会把整个文档替换成{ status: published }其他字段全丢。这个错误我见过太多次了而且它不报错静默地把你数据洗掉了非常危险。养成习惯更新永远用操作符。另外注意find返回的是游标不是数组mongosh里会自动toArray()显示前 20 条但在 Node 代码里必须自己处理。这个差异下面讲到驱动时会再提。3.3 查询操作符与投影的取舍条件查询的几个高频操作符db.posts.find({ tags: node }) // 数组包含某元素 db.posts.find({ meta.views: { $gt: 100 } }) // 嵌套字段比较 db.posts.find({ status: { $in: [draft, review] } }) db.posts.find({ title: { $regex: 路由, $options: i } }) db.posts.find({ $or: [{ status: draft }, { tags: urgent }] })投影第二个参数很多人忽略但它直接影响传输量db.posts.find({ status: published }, { title: 1, slug: 1, _id: 0 })1表示包含0表示排除。注意包含和排除不能混用唯一例外是_id可以单独排除。如果文章正文有几十 KB列表页只查标题和 slug传输量能差出一个数量级。这个小习惯在数据量上来之后收益非常明显。正则查询那条要当心$regex如果不是前缀匹配用不上索引等于全集合扫描。文章量上万之后搜索功能老老实实用全文索引或者专门的搜索组件别指望正则扛。3.4 索引用 explain 让性能差异看得见索引这事儿讲道理不如直接看数字。先造点数据for (let i 0; i 50000; i) { db.posts.insertOne({ title: 文章 i, slug: post- i, status: i % 2 ? draft : published }) }然后对比db.posts.find({ slug: post-49999 }).explain(executionStats)看executionStats里的totalDocsExamined和executionTimeMillis。没建索引时totalDocsExamined会接近 50000也就是全表扫描。建索引db.posts.createIndex({ slug: 1 }, { unique: true })再跑一次同样的explaintotalDocsExamined应该变成 1执行时间降到 1ms 以内。这个对比比任何文字描述都有说服力。unique: true这个选项也值得说。它在数据库层面保证唯一性比在应用层先查再插靠谱得多——后者在并发下存在竞态两个请求同时查到不存在然后都插入成功。数据库的唯一索引是原子的冲突时直接抛 E11000 错误你在代码里捕获就行。4. Express 骨架五行代码跑起来之后才是开始Express 上手快是公认的但快也意味着很多人跳过理解阶段直接抄模板。等到要加鉴权、加日志、加统一错误处理的时候就不知道往哪下手了。4.1 初始化工程与依赖的最小集合mkdir blog-api cd blog-api npm init -y npm i express mongoose dotenv npm i -D nodemon四个包各自的分工expressWeb 框架本体mongooseMongoDB 的对象建模工具提供 Schema、校验、中间件dotenv把.env文件里的配置读进process.envnodemon开发期监听文件变化自动重启只在开发环境用所以放devDependenciespackage.json里补上{ type: commonjs, scripts: { dev: nodemon src/server.js, start: node src/server.js } }type: commonjs这句在 Node 20 之后建议显式写上。不写的话默认也是 CommonJS但一旦项目里出现.mjs文件或者某个依赖的package.json影响了模块解析规则就可能出现require is not defined这种莫名其妙的错。显式声明能避免这一类问题。4.2 逐行拆解 app.js分成两个文件src/app.js负责组装应用src/server.js负责启动。这个拆分看起来多此一举实际上是为了后面写测试——测试的时候你只想拿到app实例丢给测试工具不想真的监听端口。// src/app.js const express require(express); const postRoutes require(./routes/post.routes); const app express(); app.use(express.json({ limit: 1mb })); app.get(/health, (req, res) { res.json({ ok: true, ts: Date.now() }); }); app.use(/api/posts, postRoutes); module.exports app;// src/server.js require(dotenv).config(); const app require(./app); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(api listening on http://127.0.0.1:${PORT}); });express.json()这行是新手最容易漏的。它是个内置中间件作用是把请求体里的 JSON 字符串解析成对象挂到req.body上。不写这行req.body永远是undefined然后你会看到一堆Cannot read properties of undefined (reading title)。limit: 1mb是请求体大小上限。默认值 100kb对于文章正文来说不够用但也不能设太大否则一个恶意请求就能把你的内存撑爆。1mb 是内容类接口比较常见的取值具体还得看你的业务。中间件的注册顺序是有意义的从上到下依次执行。express.json()必须放在所有需要读请求体的路由之前这是顺序问题不是风格问题。4.3 路由分层与中间件顺序// src/routes/post.routes.js const router require(express).Router(); const ctrl require(../controllers/post.controller); router.get(/, ctrl.list); router.get(/:slug, ctrl.detail); router.post(/, ctrl.create); router.patch(/:id, ctrl.update); router.delete(/:id, ctrl.remove); module.exports router;这里有两个细节。第一路由匹配是自上而下的。/:slug如果放在/前面不会有问题但如果你有/latest这种固定路径它必须写在/:slug之前否则latest会被当成 slug 值。这个坑在加最新文章接口的时候一定会踩到。第二router和app的区别很多初学者搞不清。app是整个应用router是一个迷你应用只关心一段路径下的子路由最后通过app.use(/api/posts, router)挂载到主应用。好处是路由文件完全不知道自己的前缀是什么换前缀只改一行的挂载代码不用动路由内部。中间件按用途大致分四类解析类express.json、urlencoded、日志类、鉴权类、错误处理类。前三类都是用app.use或router.use注册错误处理类必须写在最后且函数签名是四个参数(err, req, res, next)——少一个参数 Express 就不会把它当错误处理器。这个规则很死但没写在显眼的地方很多人第一次写错误中间件会漏掉next。4.4 nodemon 热重载与调试启动方式npm run devnodemon默认监听整个目录node_modules除外。如果你的日志文件写在项目里nodemon 会因为日志变化反复重启这时候需要加nodemon.json{ watch: [src], ext: js,json, ignore: [logs, *.log] }需要断点调试的时候node --inspect src/server.js然后在浏览器打开chrome://inspect就能看到断点面板。--inspect-brk会在第一行暂停适合排查启动阶段的初始化问题比如连接数据库时的逻辑错误。5. 打通两者连接、模型、接口一条龙前面都是分头准备这一节把线连起来。这一段跑通一个最小可用的后端就有了。5.1 官方驱动和 Mongoose 的取舍官方驱动mongodb包给的是最原始的能力client.db().collection().insertOne()跟在mongosh里敲命令几乎一样。Mongoose 是在驱动之上包了一层提供 Schema 定义、字段校验、类型转换、中间件hooks、虚拟字段这些能力。选择逻辑很简单要不要 Schema。如果你的数据结构固定、需要校验、需要关联查询用 Mongoose如果你只是把 MongoDB 当个 JSON 存储或者要做一些底层的批量操作、聚合管道直接用驱动会更直接。我个人的做法是业务代码用 Mongoose数据迁移脚本、批量修复脚本用官方驱动因为后者更接近原生命令脚本写起来更直观。5.2 连接串里那几个参数的真实作用// src/config/db.js const mongoose require(mongoose); async function connectDB() { await mongoose.connect(process.env.MONGO_URI, { maxPoolSize: 10, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 45000, }); console.log(mongodb connected); } module.exports connectDB;.env里写MONGO_URImongodb://127.0.0.1:27017/blog PORT3000这里有个特别容易踩的细节用127.0.0.1而不是localhost。在很多系统里localhost会优先解析成 IPv6 的::1而 MongoDB 默认只监听 IPv4 的127.0.0.1结果就是ECONNREFUSED。这个错误看起来像数据库没启动实际上是地址解析问题排查半天都想不到。三个参数的作用参数默认值说明maxPoolSize100连接池上限。小项目 10 就够了开太多反而拖慢数据库serverSelectionTimeoutMS30000选节点超时。默认 30 秒意味着连不上要等半分钟才报错本地调试设成 5 秒体验好很多socketTimeoutMS0不超时单次操作空闲超时。长时间不用的连接该释放掉serverSelectionTimeoutMS这一项特别影响开发体验。默认 30 秒你改代码重启如果数据库没起来要干等半分钟才看到错误很容易误以为进程卡死。5.3 Schema 设计与接口实现// src/models/post.model.js const mongoose require(mongoose); const postSchema new mongoose.Schema({ title: { type: String, required: [true, 标题必填], trim: true, maxlength: 120 }, slug: { type: String, required: true, unique: true, lowercase: true }, content: { type: String, default: }, tags: { type: [String], default: [] }, status: { type: String, enum: [draft, published], default: draft } }, { timestamps: true }); module.exports mongoose.model(Post, postSchema);{ timestamps: true }会自动维护createdAt和updatedAt省得你每次更新都手动写$currentDate。这个选项几乎每个集合都该开。enum那个字段是应用层的约束MongoDB 本身不认。好处是数据在进入数据库之前就被拦下来了报错信息也更友好。坏处是如果你通过其他方式比如 mongosh直接插数据绕过了这层校验。所以关键的业务约束最好同时写进 Schema 和数据库校验规则双保险。控制器// src/controllers/post.controller.js const Post require(../models/post.model); exports.list async (req, res, next) { try { const { status published, page 1, size 10 } req.query; const docs await Post.find({ status }) .select(title slug tags createdAt) .sort({ createdAt: -1 }) .skip((page - 1) * size) .limit(Number(size)) .lean(); res.json({ data: docs }); } catch (err) { next(err); } }; exports.create async (req, res, next) { try { const doc await Post.create(req.body); res.status(201).json({ data: doc }); } catch (err) { next(err); } };.lean()这个调用值得单独说。默认情况下 Mongoose 返回的是带了一堆内部方法的文档实例转 JSON 的时候还要跑一遍toJSON。加.lean()直接返回纯 POJO内存占用和转换开销都小不少。只读场景一律加.lean()这个习惯在列表接口上收益最明显。skip分页也要注意数据量大之后skip(100000)会非常慢因为它要遍历前面十万条。真正的深分页要用游标方式也就是基于上一页最后一条的_id或createdAt做$gt查询。练习阶段用skip没问题但要知道它的上限在哪。5.4 用请求验证整条链路curl -X POST http://127.0.0.1:3000/api/posts \ -H Content-Type: application/json \ -d {title:打通链路,slug:connect-the-dots,content:第一篇测试} curl http://127.0.0.1:3000/api/posts?statusdraft两条都通说明解析、路由、模型、连接四层都没问题。如果第一条返回 500看终端报错如果返回{data:[]}去 mongosh 里db.posts.find()确认数据到底有没有写进去。6. 四个必踩的坑与排查链路这一节是我认为整篇最有价值的部分。下面每个错误都是第一次搭这套组合时大概率会撞上的。6.1 ECONNREFUSED连不上到底怪谁MongooseServerSelectionError: connect ECONNREFUSED 127.0.0.1:27017这个错误的字面意思是目标端口拒绝连接可能的原因有四种按概率排序MongoDB 服务没启动。先systemctl status mongod或brew services list看一眼。启动了但监听的不是这个地址。bindIp配成别的地址或者端口改过。用lsof -i :27017看有没有进程在监听。地址解析问题。前面提过的localhost解析成 IPv6。容器/虚拟机网络。如果数据库跑在容器里127.0.0.1指的是容器自己要用容器名或者映射出来的宿主机地址。排查顺序建议从下往上先ping确认端口通不通再确认服务状态最后看配置。不要一上来就重装重装解决不了配置问题。6.2 认证失败的几种伪装MongoServerError: Authentication failed.如果你压根没开鉴权却报这个八成是连接串里带了个空的用户名密码段比如mongodb://127.0.0.1:27017/blogMongoose 会当成你在尝试认证。检查.env里有没有多余的或冒号。如果确实开了鉴权注意认证库。MongoDB 的用户是建在某个库下的连接时要通过authSource指定mongodb://user:pass127.0.0.1:27017/blog?authSourceadmin不写authSource默认用连接串里的库名去认证如果用户建在admin下就会失败。这个细节在配置文件里不明显但在文档的角落里有写。6.3 buffering timed out 的真相MongooseError: Operation posts.find() buffering timed out after 10000ms这个报错极具迷惑性因为它说的是操作超时但它真正想说的是数据库根本没连上。Mongoose 有个缓冲机制在连接建立之前发起的操作会先排队等连接成功后再执行。如果 10 秒内连接还没建立或者干脆失败了队列里的操作就集体超时。所以看到这个错误不要去查find语句写错了没有直接去看连接状态mongoose.connection.readyState // 0断开 1已连 2连接中 3断开中0或者2卡住就是连接问题。真正的排查方向是网络、地址、服务状态跟查询语句一点关系都没有。顺带说一句可以通过bufferCommands: false关掉这个缓冲让错误更早暴露报错也更直接。开发阶段我一般关掉它省得被误导。6.4 EADDRINUSE 与残留进程Error: listen EADDRINUSE: address already in use :::3000这通常是上一次的nodemon没退干净。查进程lsof -i :3000 netstat -ano | findstr :3000拿到 PID 之后kill -9 PID或taskkill /PID PID /F。有个更隐蔽的版本进程杀了端口还占着因为处于TIME_WAIT状态。这时候等几十秒自然释放或者把本地开发端口换一个。用nodemon的时候还有个情况是它 fork 了子进程主进程被杀了子进程还在需要pkill -f nodemon清一遍。7. 从能跑到敢用的几个习惯最后一节聊几个能跑之后马上就该补上的东西。这些不做项目不出三天就会变成一团乱麻。7.1 配置外置与环境切换所有跟环境相关的值——数据库地址、端口、密钥——都不该硬编码在代码里。.env文件负责本地配置.env.example提交到仓库作为模板.env本身写进.gitignore。这个流程看着啰嗦但它是防止密钥进版本库这类事故最有效的办法。# .env.example MONGO_URImongodb://127.0.0.1:27017/your_db PORT3000 NODE_ENVdevelopment读取的时候在入口最上方require(dotenv).config()且只调用一次。有些人在每个模块里都调一遍虽然不会报错但会让人搞不清配置到底在哪加载的。7.2 统一错误处理中间件// src/middlewares/error.js module.exports (err, req, res, next) { if (err.name ValidationError) { return res.status(400).json({ message: Object.values(err.errors).map(e e.message) }); } if (err.code 11000) { return res.status(409).json({ message: 记录已存在, field: Object.keys(err.keyPattern)[0] }); } console.error(err); res.status(500).json({ message: server error }); };挂载时必须放在所有路由之后app.use(/api/posts, postRoutes); app.use(require(./middlewares/error));11000是 MongoDB 的唯一索引冲突错误码捕获它就能把重复 slug这种问题变成 409 而不是 500。这个映射做了之后前端拿到的错误信息会精准得多。7.3 优雅关闭与连接复用process.on(SIGINT, async () { await mongoose.connection.close(); process.exit(0); });进程被 CtrlC 或者被容器编排系统终止时先关数据库连接再退出。不关的话连接池里的连接会挂着MongoDB 那边的连接数要过一段时间才会释放。本地开发影响不大但一旦部署到连接数受限的环境频繁重启会把连接数耗光。连接复用这条也顺带提一句数据库连接要在应用启动时建立一次全局复用不要在每次请求里mongoose.connect()。后者会导致连接池不断被创建和销毁性能极差而且大概率会撞上连接数上限。这套组合从安装到跑通快的话一个下午慢的话卡在某一个报错上两三天都很正常。我自己最初在buffering timed out那个错上耗了小半天把所有查询语句翻了三遍,最后才发现是mongod压根没起来。后来养成了一个习惯任何操作层面的报错先确认底层服务是不是活的再去看代码。这个顺序能省掉一大半无效排查。
阅读完成 · 觉得有帮助?