The Controller Layer
Objectives
By the end of this lesson, you should be able to:
- Write controller functions that call a model and send an appropriate response
- Import a model into a controller file and use its exported functions
- Keep a controller thin, deciding on status codes without duplicating model logic
💡 Why this matters: The controller is where
reqandresactually live, it’s the translation layer between an HTTP request and the model built in Lesson 2, and between the model’s result and an HTTP response.
⚠️ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests, across real, separate files.
A Task Controller
// controllers/taskController.js
const taskModel = require('../models/taskModel');
function index(req, res) {
res.json(taskModel.getAll());
}
function show(req, res) {
const task = taskModel.getById(Number(req.params.id));
if (!task) return res.status(404).json({ error: 'NotFound', message: 'Task not found' });
res.json(task);
}
function create(req, res) {
if (!req.body.title) {
return res.status(400).json({ error: 'ValidationError', message: 'title is required' });
}
const task = taskModel.create(req.body);
res.status(201).json(task);
}
function update(req, res) {
const task = taskModel.update(Number(req.params.id), req.body);
if (!task) return res.status(404).json({ error: 'NotFound', message: 'Task not found' });
res.json(task);
}
function remove(req, res) {
const deleted = taskModel.remove(Number(req.params.id));
if (!deleted) return res.status(404).json({ error: 'NotFound', message: 'Task not found' });
res.status(204).send();
}
module.exports = { index, show, create, update, remove };
require('../models/taskModel') (Module 3’s CommonJS, ../ navigating up one directory, Module 4’s path concepts) imports every function the model exports. Each controller function follows the exact same shape, Lesson 1’s “coordinate, don’t contain logic”: call the model, check what came back, translate that into the correct status code and response. show, update, and remove all check for a falsy result (null/false) from the model and respond with 404, this if (!result) return res.status(404)... pattern is the controller’s entire job for the “not found” case, the model already decided whether something was found, the controller only decides what that means for the response.
index Is a Naming Convention, Not a Rule
Naming the “list everything” function index (rather than getAll or list) is a common convention borrowed from other MVC frameworks, matching a resource’s collection endpoint, it’s not enforced by Express or Node.js at all, any name works, consistency across a project’s controllers matters more than the specific name chosen.
Try It
- Create
controllers/taskController.jswith the functions above, importing yourtaskModelfrom Lesson 2. - Add a
toggleDonecontroller function calling thetoggleDonemodel function from Lesson 2’s Try It, returning the updated task or a404. - Deliberately move a piece of model logic (like the
Object.assignmerge fromupdate) into the controller instead, and explain, in your own words, why this blurs the separation Lesson 1 established. - Explain, in your own words, why every controller function above takes exactly
(req, res), and wherenextwould need to be added if a function usednext(err)instead of a directres.status(...)call.
Recap
- A controller imports a model and calls its functions, translating the result into an HTTP response, it contains almost no logic of its own.
- The recurring
if (!result) return res.status(404)...pattern is the controller deciding what a model’s “not found” result means for the response, not re-implementing the lookup itself. - Naming conventions (
indexfor “list all”) come from common practice, not a language or framework requirement, consistency matters more than the specific names chosen.
Next lesson: routes, wiring the controller to real URLs, and running the complete app.