Swapping the Model Layer
Objectives
By the end of this lesson, you should be able to:
- Replace an in-memory model with a Prisma-backed one, without touching the controller
- Explain why MVC’s separation of concerns (Node.js & Express Foundations, Module 10) makes this swap possible at all
- Recognize exactly which files change, and which don’t
๐ก Why this matters: This is the moment every module in this course has been building toward, taking a real, working Express app and giving it real, persistent storage, with the smallest possible change to the code around it.
โ ๏ธ A note on verification: the Prisma-backed code below reflects Prisma Client’s stable, documented API (this course’s own tooling has no route to Prisma’s engine download, as explained starting Module 3). The MVC structure itself, and the fact that only the model file changes, is the same pattern verified end-to-end with a real, running Express server in Node.js & Express Foundations, Module 10.
Where This Starts
Node.js & Express Foundations, Module 10 built a task manager with this structure:
task-manager/
โโโ models/
โ โโโ taskModel.js
โโโ controllers/
โ โโโ taskController.js
โโโ routes/
โ โโโ taskRoutes.js
โโโ app.js
The in-memory model looked like this:
// models/taskModel.js (in-memory, Node.js & Express Foundations)
let tasks = [{ id: 1, title: 'Write lesson', done: false }];
let nextId = 2;
function getAll() {
return tasks;
}
function getById(id) {
return tasks.find(t => t.id === id);
}
function create(data) {
const task = { id: nextId++, title: data.title, done: false };
tasks.push(task);
return task;
}
function update(id, data) {
const task = getById(id);
if (!task) return null;
Object.assign(task, data);
return task;
}
function remove(id) {
const index = tasks.findIndex(t => t.id === id);
if (index === -1) return false;
tasks.splice(index, 1);
return true;
}
module.exports = { getAll, getById, create, update, remove };
The Prisma-Backed Replacement
// prisma/schema.prisma
model Task {
id Int @id @default(autoincrement())
title String
done Boolean @default(false)
}
// models/taskModel.js (Prisma-backed)
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
function getAll() {
return prisma.task.findMany();
}
function getById(id) {
return prisma.task.findUnique({ where: { id } });
}
function create(data) {
return prisma.task.create({ data: { title: data.title, done: false } });
}
function update(id, data) {
return prisma.task.update({ where: { id }, data }).catch(() => null);
}
function remove(id) {
return prisma.task.delete({ where: { id } })
.then(() => true)
.catch(() => false);
}
module.exports = { getAll, getById, create, update, remove };
Every function keeps the exact same name, and the exact same shape of return value, getAll() still returns an array of tasks, create() still returns the created task, remove() still returns true or false. Only what happens inside each function changed, an in-memory array operation became a Prisma Client call.
What Doesn’t Change at All
// controllers/taskController.js, unchanged from Node.js & Express Foundations
const taskModel = require('../models/taskModel');
async function index(req, res, next) {
try {
const tasks = await taskModel.getAll();
res.json(tasks);
} catch (err) {
next(err);
}
}
The controller was already written to handle a model function that might be asynchronous, since even the in-memory version could have been swapped for something async later. Adding await in front of taskModel.getAll() (a small, one-time update, since the in-memory version returned a value directly, not a promise) is the only controller-level change needed, the rest of the controller, the routes, and app.js are completely untouched.
Why This Was Possible
This is exactly the payoff Node.js & Express Foundations, Module 10 promised when it introduced MVC: the controller only ever calls the model’s functions, it never touches tasks (the array) or prisma.task (the client) directly. Swapping what’s behind those function names, array operations for real database queries, doesn’t ripple outward into the routes or the controller’s own logic at all.
Try It
- Take a model file from a previous project (or the in-memory version above), and rewrite each function to use Prisma Client instead, keeping every function name and return shape identical.
- Identify exactly which other files (controller, routes,
app.js) needed to change as a result. Confirm the answer is “none, except anawait.” - Explain, in your own words, why a controller that called
tasks.find(...)directly, instead oftaskModel.getById(...), would have made this swap much harder.
Recap
- Swapping an in-memory model for a Prisma-backed one means rewriting the inside of each model function, while keeping its name and return shape identical.
- The controller, routes, and
app.jsdon’t need to change at all, beyond addingawaitfor what’s now genuinely asynchronous. - This is the direct payoff of MVC’s separation of concerns, introduced in Node.js & Express Foundations specifically so a change like this would be this contained.
Next lesson: the complete API, wired end to end against a real PostgreSQL database.