A MongoDB Variant
Objectives
By the end of this lesson, you should be able to:
- Rebuild the same API’s model layer with Mongoose instead of Prisma
- Explain the one real controller-level difference MongoDB’s
_idintroduces - Compare both database-backed versions of the same API directly
💡 Why this matters: This course covered two databases for a reason, they’re both common in real backend work. Rebuilding the exact same API on MongoDB, right after Prisma, makes the similarities and the genuine differences concrete.
⚠️ A note on verification: the Mongoose-specific code below reflects Mongoose’s stable, documented API (this course’s own tooling has no route to a local MongoDB server, as explained starting Module 7). The Express routing and controller pattern is the same one verified end-to-end with a real, running Express server in Node.js & Express Foundations, Modules 9 and 10.
The Mongoose Model
// models/Task.js
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema({
title: { type: String, required: true },
done: { type: Boolean, default: false }
});
module.exports = mongoose.model('Task', taskSchema);
// models/taskModel.js
const Task = require('./Task');
function getAll() {
return Task.find();
}
function getById(id) {
return Task.findById(id).catch(() => null);
}
function create(data) {
return Task.create({ title: data.title, done: false });
}
function update(id, data) {
return Task.findByIdAndUpdate(id, data, { new: true, runValidators: true }).catch(() => null);
}
function remove(id) {
return Task.findByIdAndDelete(id)
.then(result => result !== null)
.catch(() => false);
}
module.exports = { getAll, getById, create, update, remove };
Same function names, same return shapes as both the in-memory version (Node.js & Express Foundations) and the Prisma version (Lesson 1), only the implementation changed again.
The One Real Difference: _id
PostgreSQL’s SERIAL ids are small integers (1, 2, 3…), MongoDB’s _id is a 24-character ObjectId string. This means one line in the controller needs to change:
// Prisma version (Lesson 2)
const task = await taskModel.getById(Number(req.params.id));
// MongoDB version
const task = await taskModel.getById(req.params.id);
Number(req.params.id) made sense for an integer ID, it would silently produce NaN for a MongoDB ObjectId string. Dropping the Number(...) conversion, since Mongoose’s findById accepts the string directly, is the only controller change needed, everything else, status codes, error shapes, the overall structure, stays identical to Lesson 2.
Connecting
// app.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const taskRoutes = require('./routes/taskRoutes');
const app = express();
app.use(express.json());
app.use('/api/v1/tasks', taskRoutes);
app.use((req, res) => {
res.status(404).json({ error: 'NotFound', message: 'Route not found' });
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'ServerError', message: 'Something went wrong' });
});
mongoose.connect(process.env.MONGODB_URI).then(() => {
app.listen(4701, () => console.log('Task API (MongoDB) running on port 4701'));
});
Using the API
curl -X POST http://localhost:4701/api/v1/tasks -H "Content-Type: application/json" -d '{"title":"Write MongoDB variant"}'
{"_id":"65f1a2b3c4d5e6f7a8b9c0d1","title":"Write MongoDB variant","done":false,"__v":0}
curl http://localhost:4701/api/v1/tasks/65f1a2b3c4d5e6f7a8b9c0d1
{"_id":"65f1a2b3c4d5e6f7a8b9c0d1","title":"Write MongoDB variant","done":false,"__v":0}
The response shape is close, but not identical, _id instead of id, and Mongoose’s own __v field. A real client-facing API often strips these differences out in the controller, returning a consistent shape regardless of which database sits underneath, worth doing as an exercise, not required for this lesson.
Comparing Both Variants
| Prisma (PostgreSQL) | Mongoose (MongoDB) | |
|---|---|---|
| ID field | id (integer) |
_id (ObjectId string) |
| ID in routes | Number(req.params.id) |
req.params.id directly |
| Read all | taskModel.getAll() → prisma.task.findMany() |
taskModel.getAll() → Task.find() |
| Read one | prisma.task.findUnique({ where: { id } }) |
Task.findById(id) |
| Create | prisma.task.create({ data }) |
Task.create({ ...data }) |
| Controller, routes, app.js structure | Identical | Identical |
Try It
- Build this Mongoose-backed variant, following the model, controller change, and connection above.
- Confirm every endpoint behaves the same way as the Prisma version, aside from the
_idshape. - Write a small helper in the controller that transforms
{ _id, title, done }into{ id: _id, title, done }before sending the response, hiding the database-specific field name from API consumers. - Explain, in your own words, why
Number(req.params.id)had to be removed for the MongoDB version, referencing what changed about the ID itself.
Recap
- The same MVC structure, and nearly the same model function signatures, work equally well on top of Prisma or Mongoose.
- The one genuine controller-level difference is
_id(a string) versusid(an integer), everything else in the controller, routes, andapp.jscarries over unchanged. - Choosing PostgreSQL/Prisma versus MongoDB/Mongoose for a real project comes down to the data itself (Module 9, Lesson 3’s questions), not which one this course’s MVC pattern happens to fit better, it fits both.
This is the final lesson of this module before exercises. Next: the course capstone exercise.