CodingNic

Project Structure & MVC

Exercises

Project Structure & MVC 35 min read

Exercises

Objectives

By the end of this lesson, you should be able to:

  • Refactor a single-file Express API into a complete MVC project structure
  • Correctly separate model, controller, and route concerns across real files
  • Confirm a refactored app behaves identically to its single-file starting point

⚠️ A note on verification: every command and output in this lesson was actually run with Express, across a real, multi-file project.

Exercise: Refactor a Books API Into MVC

Start from this single-file version (deliberately mixing every concern together, the way Module 9’s lessons wrote it):

javascript
const express = require('express');
const app = express();
app.use(express.json());

let books = [
  { id: 1, title: 'Clean Code', genre: 'programming', price: 35 },
  { id: 2, title: 'Dune', genre: 'sci-fi', price: 20 },
  { id: 3, title: 'Refactoring', genre: 'programming', price: 40 }
];
let nextId = 4;

app.get('/api/v1/books', (req, res) => {
  let results = books;
  if (req.query.genre) results = results.filter(b => b.genre === req.query.genre);
  res.json(results);
});

app.get('/api/v1/books/:id', (req, res) => {
  const book = books.find(b => b.id === Number(req.params.id));
  if (!book) return res.status(404).json({ error: 'NotFound', message: 'Book not found' });
  res.json(book);
});

app.post('/api/v1/books', (req, res) => {
  if (!req.body.title) return res.status(400).json({ error: 'ValidationError', message: 'title is required' });
  const book = { id: nextId++, ...req.body };
  books.push(book);
  res.status(201).json(book);
});

app.delete('/api/v1/books/:id', (req, res) => {
  const index = books.findIndex(b => b.id === Number(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'NotFound', message: 'Book not found' });
  books.splice(index, 1);
  res.status(204).send();
});

app.listen(4605);

a) Extract the model. Create models/bookModel.js, moving the books array and nextId counter into it, exporting getAll, getById, create, and remove, exactly Lesson 2’s pattern, no req/res anywhere in this file.

b) Extract the controller. Create controllers/bookController.js, importing bookModel, with index, show, create, and remove functions, each calling the model and translating the result into a response, exactly Lesson 3’s pattern. index should keep the genre filter (Module 9).

c) Extract the routes. Create routes/bookRoutes.js, mapping each path and method to its controller function with express.Router(), exactly Lesson 4’s pattern.

d) Reassemble app.js. app.js should now only: create the app, register express.json(), mount the book routes under /api/v1/books, register a 404 handler and a global error handler (Module 8), and start listening.

e) Confirm identical behavior. Test every endpoint against your refactored version, and confirm the responses exactly match what the single-file version produced:

bash
curl "http://localhost:4605/api/v1/books?genre=programming"
text
[{"id":1,"title":"Clean Code","genre":"programming","price":35},{"id":3,"title":"Refactoring","genre":"programming","price":40}]
bash
curl -w " [%{http_code}]" -X POST http://localhost:4605/api/v1/books -H "Content-Type: application/json" -d '{"title":"1984","genre":"fiction","price":15}'
text
{"id":4,"title":"1984","genre":"fiction","price":15} [201]
bash
curl -i -X DELETE http://localhost:4605/api/v1/books/3
text
HTTP/1.1 204 No Content

f) Add a second resource. Following the exact same pattern, add an authors resource (models/authorModel.js, controllers/authorController.js, routes/authorRoutes.js), mounted at /api/v1/authors, with at minimum GET / and GET /:id. Confirm it works independently, without touching any book-related file.

g) Push one step further. Pick one piece of business logic (a maximum number of books, a required minimum price, anything reasonable) and move it into a new services/bookService.js, following Lesson 6’s pattern, with the controller calling the service instead of the model directly for that one operation.

h) A multi-page version. Following Lesson 7’s pattern, build a second, server-rendered version of the same books app, views/index.handlebars (a list), views/show.handlebars (a detail page), views/new.handlebars (a create form), a shared layout, and a pre-made public/css/style.css, reusing the same bookModel.js from part (a), with a new controller and routes file rendering views and calling res.redirect() instead of res.json().

Recap

This module, and this course, close with the same idea Lesson 1 opened with, put into practice: a growing Express application needs models, controllers, and routes cleanly separated, organized into a real folder structure, with configuration, middleware, and shared error handling each in their own place, a multi-page, server-rendered app organized exactly the same way as a JSON API, and, once real business rules or a real database show up, service and repository layers ready to take on the complexity plain MVC wasn’t meant to hold.

This is the final module of Node.js & Express Foundations. From here, the next course in this track picks up databases and ORMs, connecting real models to a real PostgreSQL and MongoDB database.