skip to Main Content

Here is the userSchema:

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    trim: true,
    lowercase: true,
  },

  password: {
    type: String,
    required: true,
    trim: true,
  },
});

userSchema.pre("save", async function (next) {
  const user = this;

  console.log("before saving");

  next();
});

const User = mongoose.model("User", userSchema);

module.exports = { User };

and following is the code to update the user:

router.put("/users/:id", async (req, res) => {
  //   const user = new User(req.body);

  try {
    console.log(req.body);
    const user = await User.findByIdAndUpdate(req.params.id, req.body, {
      new: true,
      runValidators: true,
    });

    if (!user) {
      res.status(404).send();
      return;
    }

    res.status(200).send(user);
  } catch (error) {
    res.status(400).send(error);
  }
});

Since both username and password fields are required, I assumed that when I try to update the user with empty req.body, mongoose would through an error, But it isn’t.

What is the issue here?

2

Answers


  1. The behavior you’re observing is expected in Mongoose when using the findByIdAndUpdate method with the runValidators: true option. By default, Mongoose does not enforce required fields when updating documents using this method. The reason for this behavior is that Mongoose assumes you are updating an existing document, and therefore, it doesn’t treat missing fields as validation errors.

    If you want to enforce that the username and password fields are required when updating a user, you can add explicit validation checks in your route handler before calling findByIdAndUpdate. For example, you can check if req.body contains the required fields and is not empty before proceeding with the update. Here’s an updated version of your code that adds this validation:

    router.put("/users/:id", async (req, res) => {
      try {
        // Check if req.body is empty or missing required fields
        if (!req.body || !req.body.username || !req.body.password) {
          res.status(400).send("Username and password are required.");
          return;
        }
    
        // .... rest of your code
    });
    
    Login or Signup to reply.
  2. So firstly you should never send req.body as the parameter to findByIdAndUpdate. I can’t think of a good reason to send req.body as the parameter to any Model.find* method but let me illustrate why you definitely shouldn’t send it in this case.

    If your userSchema contains fields that should not be mutated by unauthorised users then they can be compromised in the req.body payload. Let’s say your schema has a field named admin that determines if user has elevated privileges. This example works for almost any field but admin privileges are a simple concept to grasp. Your schema might look like this:

    const userSchema = new mongoose.Schema({
      username: {
        type: String,
        //...
      },
    
      password: {
        type: String,
        //...
      },
    
      admin: {  
        type: Boolean,
        default: false
        //...
      },
    });
    

    Now when you pass req.body a user could send a key:value pair in the body of the put/post request with admin: true. It would look like this:

    // req.body = {username: 'bob', password: 'secret', admin:true}
    const user = await User.findByIdAndUpdate(req.params.id, req.body, {
       new: true,
       runValidators: true,
    });
    

    Now this user has admin:true stored in the document.

    Instead you should destructure the req.body and only pass the properties that you expect (after you’ve sanitised them of course but that’s a different post). This is how it would look:

    router.put("/users/:id", async (req, res) => {
      try {
        const {username, password } = req.body; // Destructure only what you expect
        const user = await User.findByIdAndUpdate(
           req.params.id, 
           {username, password}, //Only send these in the update
           {
             new: true,
             runValidators: true,
           }
        );
        //...
      } catch (error) {
        res.status(400).send(error);
      }
    });
    

    To answer your actual question. You are not alone in your assumptions. This catches nearly everyone out who uses Mongoose. The docs state:

    The other key difference is that update validators only run on the paths specified in the update. For instance, in the below example, because ‘name’ is not specified in the update operation, update validation will succeed.
    When using update validators, required validators only fail when you try to explicitly $unset the key.

    To illustrate in your example a document might look like this:

    {
       _id: new ObjectId("650ecb80507561a33ce927ad"),
       username: 'bob'
       password: 'secret'
    }
    

    Your update might look like this:

    const user = await User.findByIdAndUpdate(req.params.id, {username:'sally'}, {
       new: true,
       runValidators: true,
    });
    

    Your document now looks like:

    {
       _id: new ObjectId("650ecb80507561a33ce927ad"),
       username: 'sally' //< username updated successfully
       password: 'secret'  //< password untouched
    }
    

    Now despite your password being a required field the update works because username is the only field being updated and it met the validation (type: String, lowercase: true). Imagine having a document with hundreds of properties, nested objects inside arrays of objects etc. That is normal in MongoDB. If Mongoose did not implement this behaviour you would otherwise have to pass the whole document only to update one field.

    To summarise, by passing req.body to the findByIdAndUpdate you passed an empty object. Mongoose therefore had nothing to update and nothing to validate. The runValidators only works on fields included in the update object.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search