Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 13

Updating Documents with PUT and Handling 404s

Recommended resources

Introduction

In the last lesson, you successfully implemented the "Create" part of our CRUD API by building a POST route. You learned to handle incoming data from req.body, use Model.create() for efficient document creation, and return the correct 201 Created status code.

Now, we'll tackle the "U" in CRUD: Update. This lesson focuses on implementing a PUT route to modify an existing resource. You will learn the essential Mongoose method Model.findByIdAndUpdate(), how to use its options to get the behavior you need, and how to handle cases where the item to be updated doesn't exist by returning a 404 Not Found status.


1. The PUT Method and Route Structure

The PUT HTTP method is conventionally used to completely replace or update an existing resource. To tell the server which resource to update, we must include its unique identifier in the URL. This is done using a route parameter, just as you did for the GET by ID endpoint.

Your route will look like this: PUT /api/items/:id.

Inside the handler, you'll access the ID with req.params.id and the new data for the item from req.body.


2. Updating Documents with Model.findByIdAndUpdate()

Mongoose provides a powerful, single method for finding a document by its ID and updating it: findByIdAndUpdate. It's the most direct way to implement your PUT route.

The method signature looks like this:
Model.findByIdAndUpdate(id, update, options)

  • id: The _id of the document you want to change.
  • update: An object containing the new data for the document (e.g., req.body).
  • options: A configuration object to control the method's behavior.

There are a couple of crucial options you must know for the exam. The video below clearly explains two of the most important ones. While it uses the PATCH method, the Mongoose function and its options are identical for PUT.

#70 Update document from Express | Using MongoDB with Express| A Complete NODE JS Course

Watch this clip from the procademy channel. It explains two essential options for findByIdAndUpdate: { new: true } and { runValidators: true }. Understanding these is critical for writing correct update logic.

Watch the segment from 02:27 to 03:48. Focus on the explanation of what { new: true } does and why you need { runValidators: true } to enforce your schema rules during an update.

To summarize the key points from the video:

  • { new: true }: By default, findByIdAndUpdate returns the document as it was before the update. This is rarely what you want. Setting new: true ensures the method returns the new, modified document, which you can then send back to the client.
  • { runValidators: true }: Mongoose does not run schema validators on updates by default. This means a PUT request could save a document that violates your schema rules (e.g., missing a required field). Setting runValidators: true forces Mongoose to validate the update against your schema, ensuring data integrity.

3. Handling the "Not Found" Case (404)

What happens if a client tries to PUT data to an ID that doesn't exist in the database? In this scenario, findByIdAndUpdate will find nothing to update and will return null.

Your code must check for this null response. If you receive it, you should stop execution and immediately return a 404 Not Found status with a clear error message. This is a non-negotiable requirement for a production-grade API.

The following resource provides a perfectly clean and concise code snippet showing exactly how to implement this check.

How to Build a RESTful API Using Node, Express, and MongoDB

The article from GeeksforGeeks, 'How to Build a RESTful API...', contains a direct code example for a PUT route. Pay close attention to how it handles the response from findByIdAndUpdate.

Locate the section titled 'Update a Book By ID (PUT)'. Examine the code snippet. Specifically, look at the line if (!book) and how it returns a 404 status. This is the exact pattern you will implement.


4. Your Turn: Implement the PUT Route

Now, let's put it all together. You will add the update logic to your item router file.

Action: Open src/routes/item.routes.js and add the following PUT route handler. A logical place is after the POST route you created in the last lesson.

// src/routes/item.routes.js

// ... (keep all your existing routes)

// @route   PUT /api/items/:id
// @desc    Update an item by ID
// @access  Public
router.put('/:id', async (req, res) => {
    try {
        const { id } = req.params;
        const { name, quantity } = req.body;

        // Check if there's anything to update
        if (!name && quantity === undefined) {
            return res.status(400).json({ message: 'Request body must contain data to update.' });
        }
        
        const updatedItem = await Item.findByIdAndUpdate(
            id,
            req.body, // The new data from the request body
            { new: true, runValidators: true } // Options
        );

        // If the item with the given ID was not found
        if (!updatedItem) {
            return res.status(404).json({ message: 'Item not found.' });
        }

        // Successfully updated
        res.status(200).json(updatedItem);

    } catch (error) {
        // This will catch errors like invalid ObjectId format or validation failures
        res.status(400).json({ message: 'Error updating item', error: error.message });
    }
});

module.exports = router;

Code Breakdown:

  1. The route is defined as put('/:id', ...).
  2. We extract the id from req.params.
  3. We call await Item.findByIdAndUpdate(), passing the id, the entire req.body as the update payload, and our essential options object { new: true, runValidators: true }.
  4. Crucially, we check if updatedItem is falsy (i.e., null). If it is, we know the ID was not found, and we return a 404.
  5. If the update is successful, updatedItem will contain the modified document, which we send back to the client with a 200 OK status.
  6. The catch block will handle Mongoose errors, such as a malformed ObjectId or a schema validation error (because we set runValidators: true).

5. Testing Your Endpoint

Thorough testing is key. You need to verify all possible outcomes of your new endpoint.

  1. First, get a valid _id for an existing item by making a GET request to /api/items. Copy it.

  2. Test the Success Case (200 OK):

    • Method: PUT
    • URL: http://localhost:5001/api/items/<your_copied_id>
    • Body (raw, JSON): Provide the new data.
      {
          "name": "Gaming Mouse (Updated)",
          "quantity": 25
      }
      
    • Expected Result: A 200 OK status and the response body containing the item with its updated name and quantity.
  3. Test the Not Found Case (404 Not Found):

    • Method: PUT
    • URL: Use a validly formatted but non-existent ID. You can copy a real ID and change the last character. For example: http://localhost:5001/api/items/60d5f3f7e5b2f2b3e4f3a9e1
    • Body: Send any valid JSON body.
    • Expected Result: A 404 Not Found status with the message "Item not found.".
  4. Test the Validation Error Case (400 Bad Request):

    • Method: PUT
    • URL: http://localhost:5001/api/items/<your_copied_id>
    • Body: Send data that violates the schema (e.g., quantity as a string).
      {
          "quantity": "fifty"
      }
      
    • Expected Result: A 400 Bad Request status with a Mongoose validation error message. This confirms runValidators: true is working.

Conclusion

You have now successfully implemented the "Update" functionality in your API. You've mastered one of the most important Mongoose methods, findByIdAndUpdate, and learned to use its options to enforce best practices. Critically, you are now correctly handling not-found cases, a key requirement for robust APIs.

Key Takeaways:

  • The PUT method is used for updating resources, identified by a route parameter like :id.
  • Model.findByIdAndUpdate(id, data, options) is the primary tool for updates.
  • The { new: true } option is essential for returning the updated document.
  • The { runValidators: true } option is a best practice for maintaining data integrity.
  • If findByIdAndUpdate returns null, it means the document was not found, and you must return a 404 status.
  • A successful update that returns the new entity should have a 200 OK status.

Next Up:
With Create, Read, and Update complete, the final core CRUD operation is "Delete". In the next lesson, you will implement the DELETE route to permanently remove resources from your database.