The driver and Mongoose lessons covered how to talk to MongoDB; this one covers where that code lives in a real web application. A long-running Express server and a Next.js app with hot reloading and serverless deployment have different connection lifecycles, and getting them wrong shows up as connection storms or "too many connections" errors in Atlas. After this lesson you will be able to structure database access for an Express API, share one client safely in Next.js, query from route handlers and Server Components, and configure pools for serverless platforms.
Connect before accepting traffic so a broken database configuration fails at startup rather than on the first request, and close the connection on shutdown:
// server.js
import express from "express";
import mongoose from "mongoose";
import { productRouter } from "./routes/products.js";
const app = express();
app.use(express.json());
app.use("/api/products", productRouter);
app.use((err, req, res, next) => { // central error mapping
if (err.name === "CastError") return res.status(400).json({ error: "Invalid id" });
if (err.name === "ValidationError") return res.status(400).json({ error: err.message });
if (err.code === 11000) return res.status(409).json({ error: "Duplicate value" });
res.status(err.status ?? 500).json({ error: "Server error" });
});
await mongoose.connect(process.env.MONGODB_URI, { autoIndex: false, maxPoolSize: 20 });
const server = app.listen(3000);
process.on("SIGTERM", async () => { server.close(); await mongoose.disconnect(); process.exit(0); });Routers then use the models directly; the error middleware turns Mongoose and driver errors into proper HTTP status codes:
// routes/products.js
router.get("/:id", async (req, res, next) => {
try {
const product = await Product.findById(req.params.id).lean();
if (!product) return res.status(404).json({ error: "Not found" });
res.json(product);
} catch (err) { next(err); }
});In development Next.js re-evaluates modules on every change. A module-level new MongoClient() would create a fresh client (and pool) each time, so the sample code stores the instance on globalThis, where it survives reloads. In production the module runs once per server process anyway. The same idea applies to Mongoose:
// lib/db.ts
import mongoose from "mongoose";
const g = globalThis as unknown as { _mongoose?: Promise<typeof mongoose> };
export function dbConnect() {
g._mongoose ??= mongoose.connect(process.env.MONGODB_URI!, { bufferCommands: false });
return g._mongoose;
}Put MONGODB_URI in .env.local (never with a NEXT_PUBLIC_ prefix) and import these modules only from server code: route handlers, Server Components, Server Actions and middleware-free server utilities.
Server Components can query the database directly, without an API layer:
// app/products/page.tsx
import { db } from "@/lib/mongodb";
import { ProductList } from "./ProductList"; // a client component
export default async function ProductsPage() {
const docs = await db.collection("products").find().sort({ name: 1 }).limit(50).toArray();
const products = docs.map(d => ({ id: d._id.toString(), name: d.name, price: d.price }));
return <ProductList products={products} />;
}The mapping step matters: ObjectId, Decimal128 and Date are not serialisable as props for client components, so convert them to strings and numbers first. For writes, a Server Action or a POST route handler validates the body, calls the database and then revalidatePath("/products") so cached pages refresh.
On platforms that run each request in a short-lived function (Vercel, AWS Lambda), every warm instance holds its own pool. Keep pools small — maxPoolSize: 5 to 10 — reuse the client at module scope so warm invocations share it, and set serverSelectionTimeoutMS low enough to fail fast. Atlas enforces a connection limit per cluster tier, so hundreds of cold starts with the default pool size of 100 can exhaust it.
_id and dates.Why is the `MongoClient` stored on `globalThis` in a Next.js project?
listen(), share the connection through models or a module, and map database errors in one error middleware.globalThis so hot reloads do not multiply connections.ObjectId and Date values before sending them to client components.MONGODB_URI server-only and out of NEXT_PUBLIC_ variables.Next lesson: Authentication, Users and Role-Based Access Control — lock the deployment down with users, roles and encryption.