Express.js Mastery / Express: Letโ€™s Start Building The Natours API!

A Better File Structure

๐Ÿ“– Context

Time for a complete restructuring of the app โ€” moving from one large file into a proper multi-file structure. The goal is to split the code by responsibility: routers, controllers, and the server entry point.


๐Ÿ”น Step 1: Creating the Routes Folder

๐Ÿ“ A new routes folder is created containing:

  • ๐Ÿ“„ tourRoutes.js

  • ๐Ÿ“„ userRoutes.js

๐Ÿ“„ Cut the tourRouter From the app.js file and paste it in the routes/tourRoutes.js file:

const express = require('express');

// (route handler functions temporarily placed here too, for now)

const router = express.Router();

router
  .route('/')
  .get(getAllTours)
  .post(createTour);

router
  .route('/:id')
  .get(getTour)
  .patch(updateTour)
  .delete(deleteTour);

module.exports = router;
  • ๐Ÿ“ฅ express needs to be imported here separately, since this is now its own module with its own scope.

const express = require('express');
  • The router variable is conventionally just called router (not tourRouter) โ€” since the file name (tourRoutes.js) already makes clear which resource it belongs to.

  • ๐Ÿ“ฆ module.exports = router โ€” since only one thing is being exported from this file, module.exports is used directly (as opposed to attaching multiple things to the exports object).

  • The same structure is repeated for routes/userRoutes.js.

  • Cut this part from the app.js file add this part in the tourRoutes.js file:

๐Ÿ“ Note on Naming

  • ๐Ÿ“ The folder is called routes (plural, matching tourRoutes.js / userRoutes.js).

  • ๐Ÿ›ฃ๏ธ The variable exported from each file is called router (singular).

  • โœ… This naming distinction is intentional and just a stylistic convention.


๐Ÿ”น Step 2: Wiring the Routers Back into the Main App File

const tourRouter = require('./routes/tourRoutes');
const userRouter = require('./routes/userRoutes');
  • Cut the fs module from app.js and paste it in the tourRoutes.js file:


๐Ÿ”น Step 3: Fixing Broken File Paths After the Move

  • ๐Ÿ“‚ Since tourRoutes.js now lives inside a routes subfolder, __dirname points to a different location than before.

  • โฌ†๏ธ Any file-reading code (like reading the tours JSON file) needs its path adjusted to go up one level, then into dev-data/data/tours-simple.json.

๐Ÿ’ก Why this matters

  • ๐Ÿ“ Moving files changes what __dirname resolves to โ€” a common source of "file not found" errors during this kind of refactor.

  • ๐Ÿ”ง Some breakage during a big refactor like this is normal and expected.


๐Ÿ”น Step 4: Creating the Controllers Folder

๐Ÿ“ A new controllers folder is created containing:

  • ๐Ÿ“„ tourController.js

  • ๐Ÿ“„ userController.js

๐Ÿ’ก Why "controllers" and not "handlers"

  • ๐ŸŽฎ The functions were previously called route handlers, but the course will later introduce the MVC (Model-View-Controller) architecture pattern, where these functions are conventionally called controllers.

  • ๐Ÿ—๏ธ Naming the folder this way now sets up that terminology in advance.

๐Ÿ“„ controllers/tourController.js

const fs = require('fs');

// (tours data reading logic here)

exports.getAllTours = (req, res) => { /* ... */ };
exports.getTour = (req, res) => { /* ... */ };
exports.createTour = (req, res) => { /* ... */ };
exports.updateTour = (req, res) => { /* ... */ };
exports.deleteTour = (req, res) => { /* ... */ };
  • export controller

๐Ÿ’ก Why exports.functionName instead of module.exports

  • ๐Ÿ“ฆ This file needs to export multiple functions, not just one.

  • ๐Ÿงฉ Attaching each function directly onto the exports object allows all of them to be exported together โ€” this is a different pattern from the single-export case used in the router files.

The same is repeated for controllers/userController.js.


๐Ÿ”น Step 5: Importing Controllers into the Route Files

// routes/tourRoutes.js
const tourController = require('./../controllers/tourController');

router
  .route('/')
  .get(tourController.getAllTours)
  .post(tourController.createTour);

router
  .route('/:id')
  .get(tourController.getTour)
  .patch(tourController.updateTour)
  .delete(tourController.deleteTour);
  • The same pattern is applied to userController.js importing from userRoutes.js.

  • Import a module in userRoutes.js file and add userController in the router:

const userController = require('./../controllers/userController');

๐Ÿ’ก Why each part works:

  • ../controllers/tourController โ€” needs to move up one level (out of routes), then into controllers.

  • Since the controller file exports multiple functions onto the exports object, requiring that file returns an object (tourController) where each exported function is a property โ€” so tourController.getAllTours, tourController.createTour, etc. are all accessible.


๐Ÿ”น Step 6: Recap of the Request Flow Through the New Structure

  1. ๐Ÿ“จ A request arrives in app.js.

  2. ๐Ÿ›ฃ๏ธ Based on the URL, it enters the matching router (e.g. tourRouter).

  3. ๐ŸŽฎ Based on the specific route/method, the router calls the matching controller function (e.g. from tourController.js).

  4. ๐Ÿ“ค The controller function sends the response, completing the request-response cycle.

โœ… This is now spread across three distinct files/layers instead of one โ€” routes, controllers, and the main app file.


๐Ÿ”น Step 7: Creating server.js โ€” Separating Express from the Server

๐Ÿ’ก Why this step exists

Best practice is to keep everything Express-related in one file (app.js) and everything server-related (starting the server, database config, environment variables, error handling, etc.) in a separate main file (server.js).

๐Ÿ“„ server.js

const app = require('./app');

const port = 3000;
app.listen(port, () => {
  console.log(`App running on port ${port}...`);
});

๐Ÿ“„ export app.js

// ...all middleware, routers, and app configuration...

module.exports = app;

๐Ÿ’ก Why each part works:

  • ๐Ÿš€ The app.listen(...) call (previously at the bottom of the main file) is moved into server.js.

  • ๐Ÿ“ฆ app.js now exports the configured Express app via module.exports = app.

  • ๐Ÿ“ฅ server.js imports it using a relative path (./app), since it's a local file in the same folder.

  • โญ Going forward, server.js becomes the true entry point of the application โ€” later additions like database connections and environment variable setup will live here, since those aren't strictly Express concerns.


๐Ÿ”น Step 8: Changing the script and Testing App

Now, since server.js is the actual entry point, lets update the npm start script tag

"scripts": {
  "start": "nodemon server.js"
}

Running the app in case not already running:

npm start
  • Now send the request and check the status:


๐Ÿ“ Final File Structure

project/
โ”œโ”€โ”€ controllers/
โ”‚   โ”œโ”€โ”€ tourController.js
โ”‚   โ””โ”€โ”€ userController.js
โ”œโ”€โ”€ routes/
โ”‚   โ”œโ”€โ”€ tourRoutes.js
โ”‚   โ””โ”€โ”€ userRoutes.js
โ”œโ”€โ”€ app.js          # Express app config + middleware + mounted routers
โ”œโ”€โ”€ server.js       # Entry point โ€” starts the server
โ””โ”€โ”€ package.json

โœ… Key Takeaway

  • ๐Ÿ›ฃ๏ธ Routes define the endpoints and map them to controllers.

  • ๐ŸŽฎ Controllers contain the logic for handling requests.

  • โš™๏ธ app.js is used for Express setup (middleware and routers).

  • ๐Ÿš€ server.js starts the server and handles app initialization.

  • ๐Ÿ“ฆ Use module.exports for a single export, and exports for multiple exports.

  • ๐Ÿ“‚ Update relative paths when moving files between folders.

  • โ–ถ๏ธ Use npm start to start the application.


๐ŸŽฏ Suggested Practice

  • ๐Ÿ” Trace a request step-by-step through the new structure:

โœ… Following this flow a few times is one of the best ways to understand how Express applications are organized.