CodingNic

Project Structure & MVC

Routes and Wiring It Together

Project Structure & MVC 12 min read

Routes and Wiring It Together

Objectives

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

  • Move route definitions into their own file, mapped to controller functions
  • Mount a router onto the main app with a URL prefix
  • Assemble a model, controller, and router into one complete, working MVC app

๐Ÿ’ก Why this matters: Lessons 2 and 3 built the model and controller in isolation. This lesson connects them to real URLs and runs the complete app, proving the whole structure actually works together, not just in theory.

โš ๏ธ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests, across the complete real, multi-file project.

Project File Structure

Every file this lesson assembles, building on Lessons 2 and 3:

text
project/
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ app.js
โ”œโ”€โ”€ models/
โ”‚   โ””โ”€โ”€ taskModel.js
โ”œโ”€โ”€ controllers/
โ”‚   โ””โ”€โ”€ taskController.js
โ””โ”€โ”€ routes/
    โ””โ”€โ”€ taskRoutes.js

The Routes File

javascript
// routes/taskRoutes.js
const express = require('express');
const router = express.Router();
const taskController = require('../controllers/taskController');

router.get('/', taskController.index);
router.get('/:id', taskController.show);
router.post('/', taskController.create);
router.patch('/:id', taskController.update);
router.delete('/:id', taskController.remove);

module.exports = router;

express.Router() (Module 9’s versioning lesson introduced this) creates a self-contained set of routes, each router.get/post/patch/delete call maps a path and method (Module 5, Module 9) directly to the matching controller function, no inline logic here at all, every actual behavior lives in taskController.js.

The Entry Point

javascript
// app.js
const express = require('express');
const taskRoutes = require('./routes/taskRoutes');

const app = express();
app.use(express.json());
app.use('/tasks', taskRoutes);

app.listen(4601, () => console.log('listening on 4601'));

app.use('/tasks', taskRoutes) mounts the entire router under the /tasks prefix, every path defined inside taskRoutes.js ('/', '/:id') is relative to that prefix, router.get('/:id', ...) combined with the mount point becomes the real route GET /tasks/:id.

Running the Complete App

bash
curl http://localhost:4601/tasks
text
[{"id":1,"title":"Write lesson","done":false},{"id":2,"title":"Verify code","done":true}]
bash
curl -X POST http://localhost:4601/tasks -H "Content-Type: application/json" -d '{"title":"Ship it"}'
text
{"id":3,"title":"Ship it","done":false}
bash
curl -X PATCH http://localhost:4601/tasks/1 -H "Content-Type: application/json" -d '{"done":true}'
text
{"id":1,"title":"Write lesson","done":true}
bash
curl -i -X DELETE http://localhost:4601/tasks/2
text
HTTP/1.1 204 No Content
bash
curl -w " [%{http_code}]" http://localhost:4601/tasks/99
text
{"error":"NotFound","message":"Task not found"} [404]

Every response here traveled through all three layers correctly: app.js routed the request to taskRoutes.js, which called the matching taskController.js function, which called taskModel.js, and the result flowed back the same way, this is Lesson 1’s separation, now proven working end to end, not just described.

Try It

  1. Build this exact project structure (models/, controllers/, routes/, app.js) using your own taskModel.js and taskController.js from Lessons 2 and 3, and confirm every route works with curl.
  2. Add a GET /tasks?done=true filter (Module 9’s filtering pattern) to the model, controller, and route together, tracing the change through all three layers.
  3. Mount the same router under a different prefix (/api/tasks instead of /tasks) by changing only app.js, and confirm nothing in routes/, controllers/, or models/ needed to change.
  4. Explain, in your own words, why a path defined as router.get('/:id', ...) inside taskRoutes.js doesn’t need to know it will eventually be mounted at /tasks.

Recap

  • A routes file imports a controller and maps each path/method combination (express.Router()) directly to a controller function, no logic of its own.
  • app.use(prefix, router) mounts an entire router under a URL prefix, every path inside the router is relative to that prefix.
  • The complete request flow, app.js to routes to controller to model and back, is what proves Lesson 1’s MVC separation actually works, not just as a diagram, but as running code.

Next lesson: structuring a growing Express app, folder conventions beyond a single resource.