Persisting Data: Connecting a Database
Until now your data vanished on restart. Wire a real database into Express so your CRUD survives — the difference between a toy and a real back-end.
What you will learn
- Explain why in-memory data is not enough
- Connect Express to a database and run CRUD that persists
- Use async/await and try/catch around every database call
Why your data keeps disappearing
In the REST API lesson your tasks lived in a plain array: let tasks = [...]. That array sits in your computer’s memory (RAM) — temporary scratch space that is wiped the moment the program stops. So every time you restart the server, all the tasks are gone. That is fine for learning, but useless for a real app: a shopping cart that empties whenever the server reboots would be a disaster.
A database fixes this. It is a separate program whose whole job is to store data on disk permanently (this is called persistence) and hand it back when asked. Your Express server talks to the database over a connection, asks it to save and fetch records, and the data outlives any restart.
Note: This course teaches MongoDB in its own subject, where you will learn the database itself in depth. Here the goal is narrower but vital: see how any database plugs into Express so your routes persist data. The exact library changes (Mongoose for MongoDB, Prisma or pg for PostgreSQL), but the shape — connect once, then await queries inside your routes — is always the same.
Step 1 — connect once, when the app starts
You open the database connection a single time as the server boots, not on every request. With MongoDB the tool is Mongoose (an ODM — Object Data Modeling library — that lets you work with database records as ordinary JavaScript objects). Install it, then connect using the secret URL you keep in .env:
npm install mongooseNote: Output:
added 20-ish packages in 2s
This downloads Mongoose into node_modules/ and records it in package.json. Mongoose is the bridge between your Express code and a MongoDB database.
// db.js
const mongoose = require("mongoose");
async function connectDB() {
await mongoose.connect(process.env.MONGO_URI);
console.log("Database connected");
}
module.exports = connectDB;Reading it: mongoose.connect(...) opens the connection using process.env.MONGO_URI — the database address you stored in .env in the CORS/env lesson, so the secret never sits in your code. It returns a promise (the connection takes a moment), so we await it. We export connectDB so the main server file can call it at startup.
Step 2 — describe the shape of your data (a model)
A database needs to know what a "task" looks like. In Mongoose you write a schema (the list of fields and their types) and turn it into a model — an object with ready-made methods like find, create and findByIdAndDelete for that collection of data:
// Task.js
const mongoose = require("mongoose");
const taskSchema = new mongoose.Schema({
text: { type: String, required: true },
done: { type: Boolean, default: false }
});
module.exports = mongoose.model("Task", taskSchema);Note: Output:
(no output — this defines structure)
The schema says each task has a text (a string that is required) and a done flag (a boolean that defaults to false). mongoose.model("Task", ...) creates a Task model you call methods on. Mongoose also adds a unique _id to every record automatically, so you never invent ids yourself.
Step 3 — rewrite your routes to use the database
Now the same CRUD routes from before, but each one awaits a database method instead of touching an array. Because database calls can fail (network blip, bad data), every handler is async and wrapped in try/catch:
const express = require("express");
const connectDB = require("./db");
const Task = require("./Task");
const app = express();
app.use(express.json());
connectDB();
// READ all — now from the database
app.get("/tasks", async (req, res) => {
const tasks = await Task.find();
res.json(tasks);
});
// CREATE — saves permanently
app.post("/tasks", async (req, res) => {
try {
const task = await Task.create({ text: req.body.text });
res.status(201).json(task);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// DELETE by the database id
app.delete("/tasks/:id", async (req, res) => {
await Task.findByIdAndDelete(req.params.id);
res.json({ message: "Deleted" });
});
app.listen(3000, () => console.log("API on http://localhost:3000"));Walking through the changes: Task.find() asks the database for every task and returns a promise, so we await it. Task.create({...}) inserts a new record and hands back the saved object (now with its own _id); the try/catch catches a missing text and replies 400. Task.findByIdAndDelete(req.params.id) removes the record whose _id matches the URL. The route shapes are identical to the in-memory version — only the data source changed.
Note: Output (GET /tasks after creating two tasks, then restarting the server):
[{"id":"66...a1","text":"Learn Express","done":false},{"id":"66...b2","text":"Add a database","done":false}]
The key win: you restarted the server and the tasks are still there. They live in the database, not in RAM. Each has a database-generated _id (a long unique string) you use to read, update or delete that exact record.
Watch out: Forgetting await is the #1 beginner bug here. Without it, Task.find() returns a pending promise, not your data — so res.json(tasks) sends back {} or a strange object. If your API returns nothing useful, check that every database call is awaited inside an async function.
Tip: The pattern to memorise: connect once at startup, then inside each route await a model method (find, create, findById, findByIdAndUpdate, findByIdAndDelete) wrapped in try/catch. Master this and you can wire up any database, not just MongoDB.
Q. Why does data in a plain array disappear when the server restarts?
✍️ Practice
- Take your in-memory tasks API and swap the array for a database model, then restart the server and confirm the data is still there.
- Add an UPDATE route using
findByIdAndUpdate, wrapped intry/catch.
🏠 Homework
- Wire a "notes" resource to a database with create, read-all and delete, and prove persistence by restarting the server between requests.