DEV Community

gunjangidwani
gunjangidwani

Posted on

Data modeling And Error Handling in Mongoose/Node.js

Couple of days ago I had some issue with data modelling and in researching the topic i found so many useful things from mongoose docs I have put summerized view of it. Hope you enjoy!!

Data modeling in MongoDB is a crucial aspect of designing efficient and scalable applications. MongoDB is a NoSQL database that stores data in flexible, JSON-like documents.

Key Concepts in MongoDB Data Modeling

  • Documents: A document in MongoDB is a basic unit of data, similar to a row in a relational database. Documents are stored in collections and can have varying structures. Example:
{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "name": "John Doe",
  "age": 30,
  "address": {
    "street": "123 Main St",
    "city": "Anytown",
    "state": "CA"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Collections: A collection is a group of MongoDB documents. It is similar to a table in a relational database. Example:
[
  {
    "_id": ObjectId("507f1f77bcf86cd799439011"),
    "name": "John Doe",
    "age": 30
  },
  {
    "_id": ObjectId("507f1f77bcf86cd799439012"),
    "name": "Jane Smith",
    "age": 25
  }
]
Enter fullscreen mode Exit fullscreen mode
  • Embedded Documents: MongoDB allows embedding documents within other documents, which can be useful for representing hierarchical data. Example:
{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "name": "John Doe",
  "address": {
    "street": "123 Main St",
    "city": "Anytown",
    "state": "CA"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Arrays: Arrays can be used to store multiple values within a single document field. Example:
{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "name": "John Doe",
  "hobbies": ["reading", "hiking", "coding"]
}
Enter fullscreen mode Exit fullscreen mode

Let's dive deeper into data modeling in MongoDB by some examples with schemas. MongoDB schemas are typically defined using the Mongoose library in Node.js applications, but the concepts apply to MongoDB in general.

Example 1: Blogging Platform

Collections:

  1. Users: Stores information about users.
  2. Posts: Stores blog posts.
  3. Comments: Stores comments on blog posts.

User Schema:

const mongoose = require('mongoose');
const { Schema } = mongoose;

const userSchema = new Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const User = mongoose.model('User', userSchema);
Enter fullscreen mode Exit fullscreen mode

Post Schema:

const postSchema = new Schema({
  title: "{ type: String, required: true },"
  content: { type: String, required: true },
  author: { type: Schema.Types.ObjectId, ref: 'User', required: true },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now },
  comments: [
    {
      type: Schema.Types.ObjectId,
      ref: 'Comment'
    }
  ]
});

const Post = mongoose.model('Post', postSchema);
Enter fullscreen mode Exit fullscreen mode

Comment Schema:

const commentSchema = new Schema({
  content: { type: String, required: true },
  author: { type: Schema.Types.ObjectId, ref: 'User', required: true },
  post: { type: Schema.Types.ObjectId, ref: 'Post', required: true },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const Comment = mongoose.model('Comment', commentSchema);
Enter fullscreen mode Exit fullscreen mode

All this is fine but How does Mongoose handle schema validation?

Answer: Mongoose provides robust schema validation capabilities that allow you to define rules and constraints for your data.

Basic Validation

const mongoose = require('mongoose');
const { Schema } = mongoose;

const userSchema = new Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true, minlength: 6 },
  age: { type: Number, min: 18, max: 99 },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const User = mongoose.model('User', userSchema);
Enter fullscreen mode Exit fullscreen mode

In this example:

  • username and email are required fields.
  • password is required and must be at least 6 characters long.
  • age must be between 18 and 99.

Custom Validation Functions

Mongoose also allows you to define custom validation functions. These functions can perform more complex validation logic. For example, you can validate that an email address is in a proper format:

const userSchema = new Schema({
  username: { type: String, required: true, unique: true },
  email: {
    type: String,
    required: true,
    unique: true,
    validate: {
      validator: function(v) {
        return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v);
      },
      message: props => `${props.value} is not a valid email address!`
    }
  },
  password: { type: String, required: true, minlength: 6 },
  age: { type: Number, min: 18, max: 99 },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});
Enter fullscreen mode Exit fullscreen mode

Async Validation

Mongoose also supports asynchronous validation, which is useful for validation logic that involves database operations. For example, you might want to check if a username already exists:

const userSchema = new Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    validate: {
      isAsync: true,
      validator: function(v, cb) {
        User.findOne({ username: v }, function(err, user) {
          cb(!user);
        });
      },
      message: 'Username already exists!'
    }
  },
  email: {
    type: String,
    required: true,
    unique: true,
    validate: {
      validator: function(v) {
        return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v);
      },
      message: props => `${props.value} is not a valid email address!`
    }
  },
  password: { type: String, required: true, minlength: 6 },
  age: { type: Number, min: 18, max: 99 },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

Enter fullscreen mode Exit fullscreen mode

Built-in Validators

Mongoose provides several built-in validators for common validation needs:

  • required: Ensures the field is not null.
  • min and max: Ensures the field is within a specified range.
  • enum: Ensures the field value is one of a specified set of values.
  • match: Ensures the field value matches a regular expression.
  • unique: Ensures the field value is unique across all documents in the collection. Example with Built-in Validators
const userSchema = new Schema({
  username: { type: String, required: true, unique: true },
  email: {
    type: String,
    required: true,
    unique: true,
    match: /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/
  },
  password: { type: String, required: true, minlength: 6 },
  age: { type: Number, min: 18, max: 99 },
  gender: {
    type: String,
    enum: ['male', 'female', 'other'],
    required: true
  },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});
Enter fullscreen mode Exit fullscreen mode

Handling Validation Errors

When a validation error occurs, Mongoose throws a ValidationError. You can handle these errors in your application logic:

const user = new User({
  username: 'john_doe',
  email: '[email protected]',
  password: 'password123',
  age: 25
});

user.save((err, user) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log('User saved:', user);
});
Enter fullscreen mode Exit fullscreen mode

what about Real project - how do we handle error in real project ?

Applying validation rules in a real project using Mongoose involves several steps. You need to define your schemas with the appropriate validation rules, handle validation errors in your application logic, and ensure that your application provides meaningful feedback to the user. Below is a step-by-step guide to applying these validation rules in a real project.

Step 1: Define Your Schemas with Validation Rules

First, define your Mongoose schemas with the necessary validation rules. Here’s an example of a user schema with various validation rules:

const mongoose = require('mongoose');
const { Schema } = mongoose;

const userSchema = new Schema({
  username: {
    type: String,
    required: [true, 'Username is required'],
    unique: true,
    trim: true,
    minlength: [3, 'Username must be at least 3 characters long']
  },
  email: {
    type: String,
    required: [true, 'Email is required'],
    unique: true,
    match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Invalid email address']
  },
  password: {
    type: String,
    required: [true, 'Password is required'],
    minlength: [6, 'Password must be at least 6 characters long']
  },
  age: {
    type: Number,
    min: [18, 'Age must be at least 18'],
    max: [99, 'Age must be less than 100']
  },
  gender: {
    type: String,
    enum: ['male', 'female', 'other'],
    required: [true, 'Gender is required']
  },
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now }
});

const User = mongoose.model('User', userSchema);
Enter fullscreen mode Exit fullscreen mode

Step 2: Handle Validation Errors in Your Application Logic

When you attempt to save a document, Mongoose will automatically validate the data against the schema rules. If validation fails, Mongoose will throw a ValidationError. You need to handle these errors in your application logic.

Here’s an example of how to handle validation errors in an Express.js route:

const express = require('express');
const mongoose = require('mongoose');
const User = require('./models/User'); // Assume User model is defined in models/User.js

const app = express();
app.use(express.json());

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/mydatabase', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

app.post('/register', async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();
    res.status(201).send(user);
  } catch (error) {
    if (error instanceof mongoose.Error.ValidationError) {
      res.status(400).json({ errors: error.errors });
    } else {
      res.status(500).send('Internal Server Error');
    }
  }
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

Enter fullscreen mode Exit fullscreen mode

In this example:
The /register route attempts to create a new user based on the request body.
If validation fails, Mongoose throws a ValidationError, which is caught and handled.
The response sends a 400 status code with the validation errors.

Step 3: Provide Meaningful Feedback to the User

When validation fails, it’s important to provide meaningful feedback to the user. Mongoose’s ValidationError object contains detailed information about the validation errors. You can extract this information and send it back to the user.
Here’s an example of how to format the validation errors:

app.post('/register', async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();
    res.status(201).send(user);
  } catch (error) {
    if (error instanceof mongoose.Error.ValidationError) {
      const errors = Object.values(error.errors).map(err => ({
        path: err.path,
        message: err.message
      }));
      res.status(400).json({ errors });
    } else {
      res.status(500).send('Internal Server Error');
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

In this example:
The validation errors are extracted and formatted into an array of objects, each containing the field path and the error message.
This formatted error information is sent back to the user with a 400 status code.

Step 4: Use Middleware for Error Handling

For larger projects, it’s a good practice to use middleware for error handling. This keeps your route handlers clean and separates error handling logic.
Here’s an example of a custom error handling middleware:

const express = require('express');
const mongoose = require('mongoose');
const User = require('./models/User'); // Assume User model is defined in models/User.js

const app = express();
app.use(express.json());

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/mydatabase', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

// Custom error handling middleware
app.use((err, req, res, next) => {
  if (err instanceof mongoose.Error.ValidationError) {
    const errors = Object.values(err.errors).map(err => ({
      path: err.path,
      message: err.message
    }));
    res.status(400).json({ errors });
  } else {
    res.status(500).send('Internal Server Error');
  }
});

app.post('/register', async (req, res) => {
  const user = new User(req.body);
  await user.save();
  res.status(201).send(user);
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

Enter fullscreen mode Exit fullscreen mode

In this example:
The custom error handling middleware is added to the Express app.
This middleware catches any ValidationError and formats the errors before sending them back to the user.


Conclusion:

By defining your schemas with appropriate validation rules, handling validation errors in your application logic, and providing meaningful feedback to the user, you can effectively apply validation rules in a real project using Mongoose. This ensures that your application maintains data integrity and provides a good user experience.

Top comments (0)

OSZAR »