应全局单例复用MongoDB连接,通过globalThis缓存clientPromise,仅服务端执行,连接字符串由服务端环境变量注入,并在Route Handler和Server Action中正确调用。

如何在Next.js 14 App Router中安全复用MongoDB连接
Next.js 14 的 App Router 默认启用服务端组件和 React Server Components,getServerSideProps 和 getStaticProps 不再适用。直接在每个服务端组件或 route handler 中新建 MongoDB 连接会导致连接泄漏、性能下降甚至连接数耗尽。
正确做法是:**全局单例复用连接,且仅在服务端执行**。MongoDB Node.js 驱动本身支持连接池复用,但必须确保连接初始化逻辑不被客户端执行(否则会暴露连接字符串)。
- 在
lib/mongodb.ts(或lib/mongo.ts)中创建连接工厂函数,用globalThis缓存已建立的MongoClient - 始终用
"use server"标记调用数据库操作的函数(如getUserById),防止意外泄露到客户端组件 - 避免在
layout.tsx或page.tsx顶层直接写await client.db().collection(...)—— 这会让连接初始化混入渲染流程,难以控制生命周期 - 连接字符串必须通过
process.env.MONGODB_URI注入,且该环境变量需在.env.local中定义,并在next.config.js中显式标记为服务端可用(Next.js 14 默认不向服务端暴露所有环境变量)
为什么 connectToDatabase 必须带缓存逻辑?
MongoDB 官方驱动建议复用 MongoClient 实例,而非每次请求都 new MongoClient()。Next.js 14 的路由处理器(app/api/xxx/route.ts)和服务器组件可能高频触发,若无缓存,每调用一次就新建连接,很快触发 MongoServerSelectionError: Server selection timed out 或 ERESOLV 类错误。
典型安全缓存写法:
import { MongoClient } from 'mongodb';
const uri = process.env.MONGODB_URI!;
const options = { useNewUrlParser: true, useUnifiedTopology: true };
let client: MongoClient;
let clientPromise: Promise<MongoClient>;
if (!global._mongoClientPromise) {
client = new MongoClient(uri, options);
global._mongoClientPromise = client.connect();
}
clientPromise = global._mongoClientPromise;
export default clientPromise;
注意:global._mongoClientPromise 是 Next.js 官方推荐模式(见其 MongoDB 示例),不是 hack。它依赖 Node.js 的模块缓存 + 全局对象持久性,在 Vercel Serverless 环境中也有效(冷启动时重新初始化,热实例复用)。
在 Route Handler 中执行增删改查的注意事项
Route Handler(app/api/users/route.ts)是处理 API 请求的最常用位置,但容易忽略几个关键点:
- 必须用
export async function POST(req: Request),不能用GET处理写操作(语义错误,且部分部署平台会拦截) - 读取请求体需手动解析:
const body = await req.json(),不要依赖 Express 风格的req.body - 数据库操作必须
await,且应包裹在try/catch中返回标准 HTTP 错误(如return Response.json({ error: 'not found' }, { status: 404 })) - 避免在
POST中直接返回res.status(201).json(...)—— Next.js Route Handler 要求返回Response实例 - 对用户输入务必校验,尤其涉及
ObjectId时:用isValid(new ObjectId(id))判断是否合法,否则findOne({ _id: new ObjectId(invalidId) })会静默返回null,而非抛错
使用 server actions 操作数据库时的限制
Server Actions("use server" 函数)适合表单提交等交互场景,但它有明确约束:
- 函数必须定义在
server目录下(如actions/userActions.ts),或在app目录内以"use server"开头的文件中 - 不能在 Client Component 中直接 import 并调用 —— 必须通过
<form action="%7B...%7D"></form>或useFormState/useFormStatus触发 - Server Action 内部仍需走同一套连接复用逻辑(即导入上面的
clientPromise),不可另起连接 - 若需上传文件并存入 GridFS,注意 Server Action 不支持
multipart/form-data自动解析,需手动处理req.formData()并流式写入,否则内存溢出
真正麻烦的从来不是“连上”,而是连上之后怎么让每次查询都可靠、可测、不拖慢首屏、不泄露密钥——这些细节藏在连接复用方式、环境变量作用域、以及 Server Action 的调用链路里。


















