So far, you’ve made it to Level 2.
You started with raw Node.js — just you and the http
module, manually parsing JSON like a caveman.
Then you upgraded to Express.js — modular routes, clean middleware, and code that finally stopped screaming at you.
But now...
We bring in the big boss: MongoDB.
Because let’s face it — writing to a file is cute, but in 2025, we store data in actual databases.
And MongoDB is that one friend who’s both chill and reliable. No schemas, no drama, just documents.
What the hell is MongoDB?
MongoDB is a NoSQL document database. It stores your data in JSON-like objects (called documents) inside collections (like tables).
Think of it like this:
- SQL: You create tables, define columns, set types.
- MongoDB: You just chuck JSON in there and call it a day.
Example?
{
"_id": "66145ad99f1b42b1",
"text": "Learn MongoDB",
"createdAt": "2025-04-08T12:00:00Z"
}
No schema? No problem.
Step 1: Get MongoDB up and running
If you’re working locally:
brew tap mongodb/brew
brew install mongodb-community
brew services start mongodb-community
OR
Just use MongoDB Atlas, the cloud version (recommended):
- Go to https://www.mongodb.com/cloud
- Create a free account
- Spin up a free cluster (it’s like 512mb — enough to change the world)
- Get your connection string:
mongodb+srv://<username>:<password>@cluster0.mongodb.net/notes-app?retryWrites=true&w=majority
Don’t worry — we’ll secure it with .env
in a sec.
Step 2: Add Mongoose — your DB sidekick
Mongoose is like a translator between your Express app and MongoDB.
It gives you schemas, models, validation, and some sanity.
npm install mongoose dotenv
Update .env
:
MONGO_URI=mongodb+srv://<your-actual-uri>
PORT=3000
Then update your index.js
:
require("dotenv").config();
const express = require("express");
const mongoose = require("mongoose");
const app = express();
app.use(express.json());
mongoose
.connect(process.env.MONGO_URI)
.then(() => console.log("✅ MongoDB Connected"))
.catch((err) => console.error("❌ MongoDB Connection Error:", err));
app.use("/notes", require("./routes/notes"));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
If it logs ✅ MongoDB Connected — you're in. You’re plugged into the matrix.
Step 3: Create a Mongoose model
Inside a new folder called models
, create Note.js
:
mkdir models
touch models/Note.js
models/Note.js
:
const mongoose = require("mongoose");
const noteSchema = new mongoose.Schema(
{
text: {
type: String,
required: true,
},
},
{
timestamps: true, // adds createdAt and updatedAt
}
);
module.exports = mongoose.model("Note", noteSchema);
You’ve just created your first schema. That means:
"No more wild JSON. We got structure now."
Step 4: Connect the model to your routes
Update routes/notes.js
:
const express = require("express");
const router = express.Router();
const Note = require("../models/Note");
// GET all notes
router.get("/", async (req, res) => {
try {
const notes = await Note.find();
res.json(notes);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET one note
router.get("/:id", async (req, res) => {
try {
const note = await Note.findById(req.params.id);
if (!note) return res.status(404).json({ error: "Note not found" });
res.json(note);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// POST a new note
router.post("/", async (req, res) => {
try {
const newNote = await Note.create({ text: req.body.text });
res.status(201).json(newNote);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// DELETE a note
router.delete("/:id", async (req, res) => {
try {
const deleted = await Note.findByIdAndDelete(req.params.id);
if (!deleted) return res.status(404).json({ error: "Note not found" });
res.json({ message: "Note deleted" });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;
Now we’re not just pretending.
You’ve got full CRUD functionality backed by a real database. Welcome to the backend elite.
Bonus: Add a controller layer (optional but chef’s kiss)
You don’t want your routes doing everything, right? Let’s separate logic:
controllers/notesController.js
const Note = require("../models/Note");
exports.getAllNotes = async (req, res) => {
const notes = await Note.find();
res.json(notes);
};
exports.createNote = async (req, res) => {
const note = await Note.create({ text: req.body.text });
res.status(201).json(note);
};
Then in routes/notes.js
:
const express = require("express");
const router = express.Router();
const controller = require("../controllers/notesController");
router.get("/", controller.getAllNotes);
router.post("/", controller.createNote);
Cleaner. Scalable. Chef’s kiss.
Final Project Structure (aka: it’s alive)
express-notes-api/
├── controllers/
│ └── notesController.js
├── models/
│ └── Note.js
├── routes/
│ └── notes.js
├── .env
├── index.js
├── package.json
You’re now officially running a real-world backend.
What you just did (and probably didn't realize)
- Created a REST API
- Connected to a cloud-hosted MongoDB instance
- Used Mongoose for models, validation, and data flow
- Handled errors like a responsible dev
- Modularized routes, controllers, and models
- Created something worthy of deploying
This is exactly how you build full production backends — just remove the chaos (or don’t).
You. Are. Dangerous.
Most devs stop at a local Express API with hardcoded arrays.
But you? You just built something scalable.
You could now build a full notes app, todo manager, blog backend, or even a chat API with very similar code.
And if this feels chaotic?
Good. Growth always feels like chaos until you look back and realize it was clarity all along.
Wanna go further?
- Add authentication (JWT + bcrypt incoming?)
- Plug in MongoDB Atlas + Prisma
- Add unit tests using Jest
- Write a frontend in React or Next.js
- Deploy it to Render/Vercel with CI/CD
Peace (for now) ✌️
You built a full-stack-ready backend. Take a moment.
Close 20 tabs. Sip some water.
You’re not a beginner anymore.