Connecting Mongoose to MongoDB is the first thing your Express server does on startup — before any routes are registered and before any requests are handled. A poorly managed database connection causes silent failures, crashes under load, or an Express server that accepts requests but cannot fulfil them because MongoDB is unreachable. In this lesson you will write a production-quality database connection module, handle all connection lifecycle events correctly, implement graceful shutdown, and structure the connection code so it integrates cleanly into your Express application.
The Connection Flow
app startup
│
▼
mongoose.connect(MONGODB_URI)
│
├─ Connecting...
│
├─ Connected ──────────────────────────────────────────────────────────────┐
│ │ │
│ ▼ │
│ app.listen(PORT) ← start accepting requests only after DB is connected │
│ │
└─ Error ────────────────────────────────────────────────────────────────── ▼
log + process.exit(1)
mongoose.connect() once at startup and Mongoose manages the pool automatically. You never need to manually acquire or release connections in your route handlers — just await your Mongoose queries normally..then() callback of mongoose.connect() — or await it before calling app.listen(). This guarantees your API never accepts HTTP requests before the database connection is established. An Express server that starts without a database connection will accept requests but crash when the first DB query runs.process.env.MONGODB_URI loaded from a .env file via dotenv. Connection strings contain credentials (Atlas username, password, cluster hostname) that must never appear in Git history. If a connection string is committed to a public repository, rotate the Atlas credentials immediately.The Database Connection Module
// server/src/config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
// These options are the defaults in Mongoose 6+ — listed for clarity
// maxPoolSize: 10, // default connection pool size
// serverSelectionTimeoutMS: 5000, // fail fast if no server found
});
console.log(`MongoDB connected: ${conn.connection.host}`);
} catch (err) {
console.error(`MongoDB connection failed: ${err.message}`);
process.exit(1); // exit the Node process — cannot run without a database
}
};
module.exports = connectDB;
Connection Events
// server/src/config/db.js — with connection event listeners
const mongoose = require('mongoose');
const connectDB = async () => {
// ── Event listeners — set up before connecting ────────────────────────────
mongoose.connection.on('connected', () => {
console.log(`MongoDB connected: ${mongoose.connection.host}`);
});
mongoose.connection.on('error', (err) => {
console.error(`MongoDB connection error: ${err.message}`);
});
mongoose.connection.on('disconnected', () => {
console.warn('MongoDB disconnected');
});
mongoose.connection.on('reconnected', () => {
console.log('MongoDB reconnected');
});
// ── Graceful shutdown on SIGINT (Ctrl+C) ──────────────────────────────────
process.on('SIGINT', async () => {
await mongoose.connection.close();
console.log('MongoDB connection closed — process exiting');
process.exit(0);
});
// ── Connect ───────────────────────────────────────────────────────────────
try {
await mongoose.connect(process.env.MONGODB_URI);
} catch (err) {
console.error(`Initial MongoDB connection failed: ${err.message}`);
process.exit(1);
}
};
module.exports = connectDB;
Integration in index.js
// server/index.js
require('dotenv').config();
const express = require('express');
const connectDB = require('./src/config/db');
const app = express();
const PORT = process.env.PORT || 5000;
// ── Middleware ─────────────────────────────────────────────────────────────────
app.use(express.json());
// ── Routes ─────────────────────────────────────────────────────────────────────
app.use('/api/posts', require('./src/routes/posts'));
app.use('/api/auth', require('./src/routes/auth'));
// ── Start — connect to DB first, then start the HTTP server ───────────────────
const start = async () => {
await connectDB(); // 1. establish database connection
app.listen(PORT, () => { // 2. only then start accepting HTTP requests
console.log(`Server running → http://localhost:${PORT}`);
});
};
start().catch(err => {
console.error('Failed to start server:', err.message);
process.exit(1);
});
Connecting to Local MongoDB vs Atlas
// server/.env — local MongoDB
MONGODB_URI=mongodb://localhost:27017/blogdb
// server/.env — MongoDB Atlas
MONGODB_URI=mongodb+srv://mern_user:password@cluster0.xxxxx.mongodb.net/blogdb?retryWrites=true&w=majority
// The connection code is IDENTICAL — only the URI in .env changes
// This is why you should never hardcode the URI — one env file change
// switches between local and Atlas seamlessly
Mongoose Connection States
| State Number | State Name | Meaning |
|---|---|---|
| 0 | disconnected | Not connected — initial state or after close() |
| 1 | connected | Active connection — queries will execute |
| 2 | connecting | Attempting to connect |
| 3 | disconnecting | In the process of closing the connection |
// Check connection state at any time
console.log(mongoose.connection.readyState);
// 0 = disconnected, 1 = connected, 2 = connecting, 3 = disconnecting
// Check in a health endpoint
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
database: mongoose.connection.readyState === 1 ? 'connected' : 'disconnected',
});
});
Common Mistakes
Mistake 1 — Starting the server before connecting to MongoDB
❌ Wrong — calling app.listen() before mongoose.connect() completes:
mongoose.connect(process.env.MONGODB_URI); // async — not awaited
app.listen(5000); // starts immediately — DB not connected yet
// First request to a DB-dependent route → MongoNotConnectedError
✅ Correct — await the connection before starting the server:
await mongoose.connect(process.env.MONGODB_URI);
app.listen(5000); // only starts after DB is connected ✓
Mistake 2 — Not handling the initial connection failure
❌ Wrong — no catch on connect() — process hangs silently if Atlas is unreachable:
mongoose.connect(process.env.MONGODB_URI); // unhandled rejection
✅ Correct — always catch and exit on initial connection failure:
try {
await mongoose.connect(process.env.MONGODB_URI);
} catch (err) {
console.error(err.message);
process.exit(1); // cannot operate without database ✓
}
Mistake 3 — Calling mongoose.connect() in every model file
❌ Wrong — each model file calls connect() creating multiple connections:
// models/Post.js
mongoose.connect(process.env.MONGODB_URI); // creates a NEW connection
// models/User.js
mongoose.connect(process.env.MONGODB_URI); // another NEW connection
// Result: multiple connections, potential pool exhaustion
✅ Correct — connect once in index.js or db.js and share the singleton.
Quick Reference
| Task | Code |
|---|---|
| Connect | await mongoose.connect(process.env.MONGODB_URI) |
| Get host | mongoose.connection.host |
| Check state | mongoose.connection.readyState === 1 |
| Close connection | await mongoose.connection.close() |
| Listen for errors | mongoose.connection.on('error', handler) |
| Graceful shutdown | process.on('SIGINT', async () => { await mongoose.connection.close() }) |