Skip to main content

Welcome to a Grasp lesson

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

Lesson 15

Required Field Validation & Error Handling

Recommended resources

Introduction

In the previous lessons, you successfully built a complete set of CRUD (Create, Read, Update, Delete) endpoints. However, our error handling is still quite basic. For instance, in your POST route, if a user tries to create an item without providing a name, our current try...catch block would likely catch an error from the database and respond with a generic 500 Internal Server Error. This isn't ideal because the problem isn't a server failure; it's invalid input from the client.

This lesson directly addresses that gap. You will learn how to implement proper input validation for your POST route. Specifically, you will enforce that certain fields are required and, if they are missing, respond with a 400 Bad Request status code along with a clear, descriptive error message. This is a fundamental requirement for any professional API.


1. Defining Validation Rules in Your Schema

The foundation of data validation in Mongoose is the schema. Mongoose allows you to define validation rules directly on your model's fields. The most common rule is making a field required.

You can specify a field is required by changing its type definition from just a Type (e.g., String) to an object containing validation rules.

How to Use MongoDB with Express and Mongoose - OneUptime

The article 'How to Use MongoDB with Express and Mongoose' shows how to add validation rules like required and custom error messages directly into a Mongoose schema. This is the first and most important step.

In the 'Defining Schemas and Models' section, examine the userSchema. Notice how fields like email, name, and password are defined as objects. Pay close attention to the required property: required: [true, 'Email is required']. The array format allows you to provide a custom error message, which is exactly what we need.

Mongoose Schema Validation Example
This image from the Mongoose documentation shows how to define various validation rules, including `required` with a custom message, directly within a schema.

Your Turn: Update Your Item Schema

Now, apply this to your project.

  1. Open your item model file: src/models/Item.model.js.
  2. Modify the name field to make it required and provide a custom error message.
// src/models/Item.model.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const ItemSchema = new Schema({
    name: {
        type: String,
        required: [true, 'Item name is required.'] // Update this line
    },
    // Add other fields from your schema here if you have them
}, {
    timestamps: true
});

module.exports = mongoose.model('Item', ItemSchema);

With this change, Mongoose will now automatically throw a ValidationError if you attempt to create an Item without a name. Our next step is to catch this specific error and handle it correctly.


2. Catching ValidationError in Your Route

Currently, your POST route's catch block is generic. We need to make it intelligent enough to identify a Mongoose ValidationError and treat it differently from other server errors.

When Mongoose validation fails, the error object it throws has a name property set to 'ValidationError'. We can use this to create a specific if condition within our catch block.

How to Use MongoDB with Express and Mongoose - OneUptime

The 'OneUptime' article also provides the exact pattern for handling this in an Express route. It demonstrates how to add a conditional check inside the catch block.

Locate the code for the POST / route under the 'Create' heading. In the catch (error) block, notice the check: if (error.name === 'ValidationError'). This is the key to distinguishing validation failures from other errors.

The error object for a ValidationError contains a nested errors object. This object holds the details for each field that failed validation. To create a clean response for the client, we can extract the message from each of these validation failures.

A common and efficient pattern is to use Object.values() and .map():
Object.values(error.errors).map(e => e.message)

This turns the complex error object into a simple array of strings, like ["Item name is required."] which is perfect for a JSON response.


3. Your Turn: Implement the Validation Handler

Let's update your POST route to implement this robust validation handling.

Action: Open your routes file, src/routes/item.routes.js, and replace your existing POST route with this updated version.

// src/routes/item.routes.js

// ... (keep your router setup and GET routes)

// @route   POST /api/items
// @desc    Create a new item
// @access  Public
router.post('/', async (req, res) => {
    try {
        const newItem = new Item({
            name: req.body.name,
        });

        const item = await newItem.save();
        res.status(201).json(item);

    } catch (error) {
        // Handle Mongoose validation errors
        if (error.name === 'ValidationError') {
            // Extract validation error messages
            const messages = Object.values(error.errors).map(err => err.message);
            return res.status(400).json({
                message: 'Validation failed.',
                errors: messages
            });
        }

        // For all other errors, send a generic 500 status
        res.status(500).json({ message: 'Error creating item', error: error.message });
    }
});

// ... (keep your PUT and DELETE routes)

module.exports = router;

Code Breakdown:

  • The try block remains the same: it attempts to save a new item.
  • The catch block now has a crucial if (error.name === 'ValidationError') check.
  • If it is a validation error, we extract the messages and return a 400 Bad Request status. The JSON response is structured to be helpful, providing a general message and an array of specific error strings.
  • If it's any other kind of error, we fall back to the 500 Internal Server Error response, just as before.

4. Testing Your Validation Logic

You must test both the failure and success paths to confirm your logic is correct.

Postman POST Request for Employee Creation
This image shows how to construct a POST request in Postman. To test our validation, you will send a request with an incomplete or empty JSON body.
  1. Start your server: npm run dev.

  2. Test the Failure Case (400 Bad Request):

    • Method: POST
    • URL: http://localhost:5001/api/items
    • Body: Select raw and JSON. Send an empty object: {}.
    • Expected Result: You should receive a 400 Bad Request status code. The response body should look like this:
      {
          "message": "Validation failed.",
          "errors": [
              "Item name is required."
          ]
      }
      
  3. Test the Success Case (201 Created):

    • Method: POST
    • URL: http://localhost:5001/api/items
    • Body: Send a valid object: { "name": "My New Valid Item" }.
    • Expected Result: You should receive a 201 Created status code, and the body will contain the newly created item with its _id and timestamps.

Conclusion

You have significantly improved the robustness of your API. By adding validation rules to your Mongoose schema and implementing specific error handling for ValidationError, you now provide clear, actionable feedback to your API's consumers instead of opaque server errors.

Key Takeaways:

  • Validation rules like required are defined directly in your Mongoose schema. You can include custom error messages: required: [true, 'This field is required.'].
  • When save() or create() is called with invalid data, Mongoose throws an error where error.name is 'ValidationError'.
  • You should specifically catch this error in your route handlers to return a 400 Bad Request status.
  • The detailed error messages can be extracted from the error.errors object, for example with Object.values(error.errors).map(e => e.message).

Next Up:
Our error handling is better, but there's another common client error we're not handling gracefully yet. If a user provides a malformed ID in the URL (e.g., /api/items/123), Mongoose throws a CastError, and our app currently responds with a generic 500 error. In the next lesson, you'll learn to catch this specific error and return an appropriate 400 status, further professionalizing your API.