CodingNic

Project Structure & MVC

The Controller Layer

Project Structure & MVC 12 min read

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 req and res actually 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

javascript
// 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

  1. Create controllers/taskController.js with the functions above, importing your taskModel from Lesson 2.
  2. Add a toggleDone controller function calling the toggleDone model function from Lesson 2’s Try It, returning the updated task or a 404.
  3. Deliberately move a piece of model logic (like the Object.assign merge from update) into the controller instead, and explain, in your own words, why this blurs the separation Lesson 1 established.
  4. Explain, in your own words, why every controller function above takes exactly (req, res), and where next would need to be added if a function used next(err) instead of a direct res.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 (index for “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.