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

Handling POST Requests

πŸ”Ή Step 1: Add the POST route

app.post('/api/v1/tours', (req, res) => {
  // route handler
});
  • Same URL as the GET route (/api/v1/tours) β€” only the HTTP method differs.

  • GET β†’ fetch all tours; πŸ“€ POST β†’ create a new tour.

  • Callback gets req and res, as usual.


πŸ”Ή Step 2: Why req.body is Empty by Default

  • πŸ“€ req.body stores the data sent by the client in a POST request.

  • ⚠️ By default, Express doesn't populate req.body. The request body isn't parsed automatically.

Example

app.post('/api/v1/tours', (req, res) => {
  console.log(req.body);

  res.send('Done');
});

If the client sends:

{
  "name": "The Forest Hiker",
  "duration": 5
}

The console prints:

undefined

Even though the client sent JSON, Express hasn't parsed it yet.

βš™οΈ Middleware is responsible for processing incoming requests (such as parsing JSON) before they reach the route handler. We'll cover middleware in detail later.


πŸ”Ή Step 3: Add the JSON Body-Parsing Middleware

app.use(express.json());
  • πŸ“ Placed near the top of the file, before the routes.

  • 🧩 app.use() is how middleware gets applied to the app.

  • πŸ“¦ This specific middleware (express.json()) parses incoming JSON request bodies and attaches the result to req.body.


πŸ”Ή Step 4: Verify It Works

app.post('/api/v1/tours', (req, res) => {
  console.log(req.body);
  res.send('Done');
});
  • πŸ“ res.send('Done') is just a temporary placeholder response β€” some response must always be sent to complete the request/response cycle.


πŸ”Ή Step 5: Testing in Postman

  • πŸ“ Created a Postman collection called "Natours" and saved both requests inside it: "Get all tours" and "Create new tour."

  • πŸ“¦ For the POST request body: select Body β†’ raw β†’ JSON, then write actual JSON:

{
  "name": "Test Tour",
  "duration": 10,
  "difficulty": "easy"
}
  • JSON requires double quotes for all keys/strings.

  • Sending this request: response = "Done", and the console logs the parsed object from req.body β€” confirming it's a real JS object.

  • Proof middleware is required: commenting out app.use(express.json()) causes req.body to become undefined. Re-adding it fixes it.


πŸ”Ή Step 6: Persist the New Tour to the JSON File

πŸ’Ύ The tours-simple.json file is currently acting as a fake/fictional database, so the goal is to actually write the new tour into it.

βœ… Generate a new ID

const newId = tours[tours.length - 1].id + 1;
  • In REST, the client never sends the ID for a new resource β€” normally a real database auto-generates it.

  • Since there's no real database yet, the workaround is: take the ID of the last tour in the array and add 1.

βœ… Merge the new ID with the request body

const newTour = Object.assign({ id: newId }, req.body);
  • Object.assign(target, source) merges objects together β€” here, merging { id: newId } with the data from req.body.

  • Could have done req.body.id = newId instead, but this approach avoids mutating the original req.body object.

βœ… Add the new tour to the in-memory array

Reference

const tours = JSON.parse(
  fs.readFileSync(`${__dirname}/dev-data/data/tours-simple.json`)
);
tours.push(newTour);
  • πŸ“š tours is the array already loaded from the JSON file at startup.

βœ… Write the updated array back to the file

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

βœ… Writing the Updated Data

  • ⚠️ Use fs.writeFile(), not fs.writeFileSync().

    • Route handlers run as part of the event loop, so blocking synchronous operations should be avoided.

  • πŸ”„ fs.writeFile()

    • Writes the file asynchronously in the background.

    • Once the write is complete, its callback is executed.

  • πŸ”„ JSON.stringify(tours)

    • Converts the JavaScript array into a JSON string before saving it to the file.

  • βœ… Send the response inside the callback

    • This ensures the file has been written successfully before responding to the client.

  • 🟒 res.status(201)

    • 201 Created indicates that a new resource was created successfully.

  • πŸ“¦ JSend Response

    • status: 'success'

    • data: { tour: newTour }

  • πŸ“‹ No results field

    • Since only a single object is returned, a count isn't needed. results is mainly useful when returning arrays.


πŸ”Ή Step 7: Fixing the "Two Responses" Error

⚠️ If you try this endpoint now, you'll get the following error:

Cannot set headers after they are sent to the client

πŸ” Cause

  • The route is sending two responses:

    • res.send('Done')

    • res.status(201).json(...)

  • A single request can only have one response.

βœ… Fix

  • Remove the old res.send('Done').

  • Keep only the res.status(201).json(...) response.


πŸ”Ή Step 8: Confirming It Works

  • βœ… Retesting the POST request with the "Test Tour" body: response now correctly returns an auto-generated id: 10 along with the submitted tour data.

  • Payload

    {
      "name": "Test Tour",
      "duration": 10,
      "difficulty": "easy"
    }
  • πŸ” Sending GET /api/v1/tours right after does not immediately show the new tour β€” unless the server restarts.


πŸ”Ή Step 9: Verify the New Tour

  • βœ… Initially, the Get All Tours endpoint returned tours up to ID 8.

  • βž• After creating a new tour, calling the endpoint again now returns ID 9 as well.

πŸ“ Note

  • πŸ“– The tours array is loaded only once when the server starts.

  • πŸ”„ Since we're using Nodemon, saving any file automatically restarts the server.

  • βœ… On restart, Express reads the updated tours-simple.json, so the new tour appears in the Get All Tours response.

  • ⚠️ Without Nodemon, you'd need to restart the server manually to see the newly added tour.


βœ… Key Takeaway

  • πŸ“€ POST route uses the same URL as GET, differing only by HTTP method.

  • πŸ“¦ req.body requires the express.json() middleware (app.use(express.json())) to exist at all.

  • πŸ—οΈ New resource creation pattern (in absence of a real database): generate an ID manually, merge it with the request body, push into the in-memory array, then persist with fs.writeFile (async, never writeFileSync, inside a route handler).

  • 🟒 Correct status code for creation is 201, not 200.

  • ⚠️ Only ever send one response per request β€” sending two throws a headers-already-sent error.

  • πŸ”„ With nodemon, saving code triggers a restart, which re-reads data from the file β€” this is why newly created data appears to only "show up" after a save/restart in this dev setup.