Express.js Mastery / Express: Let’s Start Building The Natours API!

Refactoring Our Routes

πŸ“– Context

Right now, all routes are defined one after another with their handler functions written inline (directly as callbacks). This makes it hard to see, at a glance, what routes exist in the app because the route definitions and the handler logic are mixed together.

🎯 The goal of this refactor is to:

  1. 🧩 Separate the handler functions from the routes themselves.

  2. πŸ”— Group related HTTP methods for the same URL together using app.route().


πŸ”Ή Step 1: Extract each inline callback into a named function

Instead of writing the callback directly inside app.get(), app.post(), etc., each handler is pulled out into its own named function. Just create new functions for //GET ALL TOURS ,//GET TOUR ,//CREATE TOUR ,//UPDATE TOUR ,//DELETE TOUR

//GET ALL TOURS
const getAllTours = (req, res) => {
   res.status(200).json({
    status: 'success',
    results: tours.length,
    data: {
      tours: tours,
    },
  });
};

//GET TOUR
const getTour = (req, res) => {
  console.log(req.params);

 const id = req.params.id * 1; // string β†’ number conversion trick

const tour = tours.find((el) => el.id === id);

// if(id > tours.length) {
if(!tour) {
  return res.status(404).json({
    status: 'fail',
    message: 'Invalid ID',
  });
}

  res.status(200).json({
    status: 'success',
    data: {
      tour,
    },
  });
};

//CREATE TOUR
const createTour = (req, res) => {
 // console.log(req.body);

const newId = tours[tours.length - 1].id + 1;
const newTour = Object.assign({ id: newId }, req.body);

tours.push(newTour);

fs.writeFile(
  `${__dirname}/dev-data/data/tours-simple.json`,
  JSON.stringify(tours),
  (err) => {
    res.status(201).json({
      status: 'success',
      data: {
        tour: newTour,
      },
    });
  }
);
};

//UPDATE TOUR
const updateTour = (req, res) => {
   if (req.params.id * 1 > tours.length) {
    return res.status(404).json({
      status: 'fail',
      message: 'Invalid ID'
    });
  }

  res.status(200).json({
    status: 'success',
    data: {
      tour: '<Updated tour here...>'
    }
  });
};

//DELETE TOUR
const deleteTour = (req, res) => {
  if (req.params.id * 1 > tours.length) {
    return res.status(404).json({
      status: 'fail',
      message: 'Invalid ID'
    });
  }

  res.status(204).json({
    status: 'success',
    data: null
  });
};
  • Then the routes simply reference these functions by name instead of containing the full logic inline:

app.get('/api/v1/tours', getAllTours);
app.get('/api/v1/tours/:id', getTour);
app.post('/api/v1/tours', createTour);
app.patch('/api/v1/tours/:id', updateTour);
app.delete('/api/v1/tours/:id', deleteTour);

πŸ’‘ Why this works:

  • βœ… The functionality doesn't change at all β€” only the code structure changes.

  • 🏷️ Naming each function after what it does (getTour, createTour, updateTour, deleteTour) makes the code more self-explanatory β€” reading app.patch('/api/v1/tours/:id', updateTour) immediately tells you what happens without needing to read the full logic.

  • πŸ“š All the handler functions are now grouped together, separate from the route definitions, making the overall route list easier to scan.


πŸ”Ή Step 2: Improve further with app.route()

❌ The problem:

With the current approach, if the resource name or API version ever changes (e.g., /api/v1/tours β†’ /api/v2/tours), that URL string would need to be updated in five separate places β€” not ideal.

βœ… The solution:

Use app.route() to chain multiple HTTP methods to a single URL definition.

app
  .route('/api/v1/tours')
  .get(getAllTours)
  .post(createTour);

app
  .route('/api/v1/tours/:id')
  .get(getTour)
  .patch(updateTour)
  .delete(deleteTour);

πŸ’‘ Why this works:

  • πŸ”— The URL is written once per route group, then methods are chained onto it.

  • πŸ“‹ /api/v1/tours only ever needs GET (all tours) and POST (create a tour) β€” no :id needed since these operations don't target a specific tour.

  • 🎯 /api/v1/tours/:id needs GET (single tour), PATCH (update), and DELETE β€” all operations that require a specific tour's ID.

  • πŸ› οΈ This is more maintainable: changing the URL now only requires an edit in one place per route group.

  • ✨ The code is also more readable/nicely formatted since related methods are visually grouped under their shared URL.


πŸ”Ή Step 3: Verifying it still works

After refactoring:

  • βœ… Testing getTour (with a valid ID) β€” works with no errors, same as before.

  • βœ… Testing getAllTours β€” also works as expected.

πŸ’‘ Since only the structure changed (not the logic itself), all existing behavior is preserved.


βœ… Key Takeaway

  • 🧩 Separating route handlers into named functions makes route definitions self-documenting and keeps handler logic organized in one place.

  • πŸ”— app.route(url).get(...).post(...) (method chaining) eliminates URL repetition β€” a single URL is now defined once and reused across multiple HTTP methods.

  • πŸ“ This refactor sets up the code for a future improvement: exporting these handler functions into a separate file, keeping the main routing file even cleaner.

  • πŸš€ No functional/behavioral change occurred β€” this was purely a structural/organizational improvement (a classic refactor)