Connecting Express to MongoDB

intermediate
15 min

Connecting Express to MongoDB

Everything we have stored so far lived in arrays that vanish when the server restarts. Time to fix that. MongoDB is a document database that stores JSON-like records, making it a natural partner for Express. We will connect through Mongoose, the most popular MongoDB library for Node, define a schema, and rewrite our to-do endpoints to persist data.

Why MongoDB and Mongoose?

MongoDB stores documents that look like JavaScript objects, so there is no translation layer between your API payloads and your database rows. Mongoose adds two things raw drivers lack: schemas that validate documents before they are saved, and a model API with friendly methods like find, create, and findByIdAndUpdate.

Getting a Database

You have two easy options. Locally, install MongoDB Community Server and it listens on mongodb://127.0.0.1:27017. In the cloud, MongoDB Atlas has a free tier: create a cluster, add a database user, allow your IP, and copy the connection string. Either way, keep the string in an environment variable, never in code:

bash
npm install mongoose dotenv

Create a .env file (and add it to .gitignore):

MONGODB_URI=mongodb://127.0.0.1:27017/express_tutorial

Connecting on Startup

js
require('dotenv').config(); const express = require('express'); const mongoose = require('mongoose'); const app = express(); app.use(express.json()); async function start() { await mongoose.connect(process.env.MONGODB_URI); console.log('MongoDB connected'); app.listen(3000, () => console.log('API on port 3000')); } start().catch(err => { console.error('Failed to start:', err.message); process.exit(1); });

Connecting before listening means the app never accepts traffic it cannot serve. If the database is down, the process exits loudly instead of failing on every request.

Defining a Schema and Model

js
// models/Todo.js const mongoose = require('mongoose'); const todoSchema = new mongoose.Schema({ title: { type: String, required: true, trim: true, maxlength: 200 }, done: { type: Boolean, default: false }, }, { timestamps: true }); module.exports = mongoose.model('Todo', todoSchema);

The schema enforces rules at the data layer: title is mandatory, whitespace is trimmed, and timestamps adds createdAt and updatedAt automatically. The model Todo maps to a todos collection.

CRUD with the Model

js
const Todo = require('./models/Todo'); app.get('/api/todos', async (req, res) => { const todos = await Todo.find().sort({ createdAt: -1 }); res.json(todos); }); app.post('/api/todos', async (req, res) => { const todo = await Todo.create({ title: req.body.title }); res.status(201).json(todo); }); app.patch('/api/todos/:id', async (req, res) => { const todo = await Todo.findByIdAndUpdate( req.params.id, { done: req.body.done }, { new: true, runValidators: true } ); if (!todo) return res.status(404).json({ error: 'Not found' }); res.json(todo); });

Every model method returns a promise, so handlers become async functions. The new: true option makes updates return the modified document rather than the original.

When Things Go Wrong

If a client posts an empty title, Todo.create rejects with a ValidationError. In Express 5, errors thrown in async handlers are forwarded to the error-handling middleware automatically, which we will build properly in lesson 12. Malformed IDs raise a CastError, which you should translate to a 400 response.

Key Takeaways

  • Mongoose adds schemas, validation, and a clean model API on top of MongoDB.
  • Store the connection string in .env and load it with dotenv; never commit it.
  • Connect to the database before calling app.listen.
  • Schemas enforce required fields, defaults, and timestamps at the data layer.
  • Model methods are async; use await inside async route handlers.

Next lesson: Building a REST API with Express, where we assemble these pieces into a complete, well-designed API.

Connecting Express to MongoDB - Express.js | CodeYourCraft | CodeYourCraft