Mongoose Schema Explained: Tips and Best Practices

Priyanshu PandeyPriyanshu Pandey
10 min read

In Mongoose, everything begins with a Schema. Each schema links to a MongoDB collection and defines the structure of the documents in that collection. This means that with a schema, we decide what type of data will be in the collection and what format that data will follow.

What is a SchemaType?

A SchemaType in Mongoose is like a blueprint that tells Mongoose what kind of data a field can hold and any rules it must follow.

For example, you can set a field to be a String, Number, or Date, and add rules like "this field is required," "this number must be positive," or "this string should be unique." SchemaTypes help ensure that data in your database is stored in a consistent and valid format.

What is type?

The type option specifies the data type for a field (e.g., String, Number, Boolean, Date, ObjectId, Array, etc.)

What is SchemaType Options ?

SchemaType options are additional settings that define constraints, defaults, and behaviors for each field. These options are used alongside type to enforce rules or add functionality to individual fields. Here are some common SchemaType options:

required: Ensures the field is present in the document. If required: true, Mongoose will throw an error if the field is missing.

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  age: { type: Number, required: true }
});

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

module.exports = User;

This snippet creates a Mongoose schema for a User model with an age field that must be present and must be a number.

default: Sets a default value if the field is not provided.

isActive: { type: Boolean, default: true }

isActive: { type: Boolean, default: true }

unique: Ensures each value in this field is unique across all documents.


const userSchema = new mongoose.Schema({ 
  email: { type: String, unique: true, required: true },
});

enum: To use the enum option in a Mongoose schema, you can define a field that only accepts a specific set of values.

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  status: {
    type: String,
    enum: ['active', 'inactive', 'pending'],
    required: true
  }
});

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

module.exports = User;

In this example, the status field in the userSchema can only have one of the three values: 'active', 'inactive', or 'pending'. If you try to save a document with a different value for status, Mongoose will throw a validation error.

validate: To use the validate option in a Mongoose schema, you can define custom validation logic for a field.

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    validate: {
      validator: function(v) {
        return /^[a-zA-Z0-9]+$/.test(v); // Only allows alphanumeric characters
      },
      message: props => `${props.value} is not a valid username!`
    }
  },
  age: {
    type: Number,
    validate: {
      validator: function(v) {
        return v >= 18; // Age must be at least 18
      },
      message: props => `${props.value} is not a valid age! Age must be at least 18.`
    }
  }
});

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

module.exports = User;

In this example, the username field is validated to ensure it only contains alphanumeric characters, and the age field is validated to ensure it is at least 18. If the validation fails, a custom error message is provided.

objectId: The ObjectId SchemaType is particularly useful for creating relationships between documents in MongoDB. It allows you to reference one document from another, which is similar to a "foreign key" in relational databases Here's a code demonstrating how to use ObjectId in a Mongoose schema:

Example : User and Post Relationship

Situation: You’re building a blog application where each user can create multiple posts. Instead of storing all post data within the user document (which would be inefficient and make the user document large), you can use ObjectId to link each post to its author.

Schema Design:

  • User Schema: Holds user information.

  • Post Schema: Holds the post content and references the User who created it with an ObjectId.

Here's a code for creating a User and Post schema in Mongoose, where each post references a user as its author:

// User schema
const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, unique: true, required: true }
});

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

// Post schema
const postSchema = new mongoose.Schema({
  title: { type: String, required: true },
  content: { type: String, required: true },
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } // References the User model
});

const Post = mongoose.model('Post', postSchema);

This code defines two Mongoose schemas: one for users and one for posts. The author field in the postSchema uses ObjectId to reference the User model, establishing a relationship between the two collections.

// Example: Creating a user and a post
const user = new User({ name: 'Alice', email: 'alice@example.com' });
await user.save();

const post = new Post({
  title: 'My First Post',
  content: 'This is the content of my first post.',
  author: user._id // Storing the reference to the user
});
await post.save();

// Populate author details when retrieving the post
const savedPost = await Post.findById(post._id).populate('author');
console.log(savedPost);

In this example, a new user and a post are created and saved to the database. When retrieving the post, the populate method is used to include the author details in the result.

index:In Mongoose, an index is used to improve the performance of queries on a collection. Indexes can be created on one or more fields of a schema to speed up the retrieval of documents.

Simple Understanding Situation

Situation: You have a collection of users, and you frequently search for users by their email addresses. Without an index, MongoDB would have to scan every document in the collection to find the matching email, which can be slow if the collection is large. By creating an index on the email field, MongoDB can quickly locate the documents with the specified email.

const schema2 = new Schema({
  test: {
    type: String,
    index: true,
    unique: true // Unique index. If you specify `unique: true`
    // specifying `index: true` is optional if you do `unique: true`
  }
});

Alternative Way to Define Indexes

Here's how you can define an index on the email field in a Mongoose schema:

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: String,
  email: { type: String, unique: true }, // Unique index on email
  age: Number
});

// Create an index on the email field
userSchema.index({ email: 1 });

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

// Example usage
async function findUserByEmail(email) {
  const user = await User.findOne({ email });
  return user;
}

In this example, the userSchema has an index on the email field. The unique: true option ensures that each email is unique across all documents, and the userSchema.index({ email: 1 }) line explicitly creates an index on the email field to optimize query performance.

String->

  • lowercase: If set to true, it will change the value to lowercase letters. For example, "HELLO" becomes "hello".

  • uppercase: If set to true, it will change the value to uppercase letters. For example, "hello" becomes "HELLO".

  • trim: If set to true, it will remove any spaces from the beginning and end of the value. For example, " hello " becomes "hello".

  • match: You can use a regular expression (a pattern) to check if the value fits a certain format. For example, it can ensure an email address is in the correct format.

  • enum: You can specify an array of valid values, and the value must be one of those. For example, if you have colors like ["red", "green", "blue"], the value must be one of these.

  • minLength: Sets a minimum number of characters that the value must have. For example, if you set it to 5, the value must have at least 5 characters.

  • maxLength: Sets a maximum number of characters that the value can have. For example, if you set it to 10, the value can have at most 10 characters.

  • populate: Used to set default options for populating related data in Mongoose (more advanced feature).

Number:

min: This sets a minimum value that the number must be greater than or equal to. For example, if you set min: 1, the number cannot be less than 1.

  • max: This sets a maximum value that the number must be less than or equal to. For example, if you set max: 100, the number cannot be greater than 100.

  • enum: This allows you to specify a list of valid numbers. The value must exactly match one of the numbers in the array. For example, if you have enum: [1, 2, 3], the value can only be 1, 2, or 3.

  • populate: This is used to set default options for populating related data in Mongoose (more advanced feature).

Date:

min: This sets the earliest date that can be entered. If you set min: new Date('2020- 01-01'), the date cannot be earlier than January 1, 2020.

  • max: This sets the latest date that can be entered. If you set max: new Date('2025-12-31'), the date cannot be later than December 31, 2025.

  • expires: This is used to automatically delete documents from the database after a certain amount of time. You can set it as a number (in seconds) or a string. For example, if you set expires: '1d', the document will be deleted one day after its date.

Map : a Map is a SchemaType that allows you to store arbitrary key-value pairs of data. It is useful when you need to store dynamic data where the keys are not known in advance.

Situation: You are developing a multi-language application where each user can have settings in different languages. You want to store these settings as key-value pairs, where the key is the language code and the value is the setting.

const userSchema = new mongoose.Schema({
  name: String,
  languageSettings: {
    type: Map,
    of: String
  }
});

// Example usage
const user = new User({
  name: 'Alice',
  languageSettings: {
    en: 'English setting',
    fr: 'French setting'
  }
});

Buffer

Situation: You are building a photo-sharing application where users can upload profile pictures. You need to store these images in the database as binary data.

const profileSchema = new mongoose.Schema({
  username: String,
  profilePicture: {
    data: Buffer,
    contentType: String
  }
});

// Example usage
const profile = new Profile({
  username: 'john_doe',
  profilePicture: {
    data: fs.readFileSync('path/to/image.jpg'),
    contentType: 'image/jpeg'
  }
});

Arrays

Situation: You are creating a blogging platform where each post can have multiple tags. You want to store these tags as an array of strings.

const postSchema = new mongoose.Schema({
  title: String,
  content: String,
  tags: [String]
});

// Example usage
const post = new Post({
  title: 'Introduction to Mongoose',
  content: 'This is a post about Mongoose.',
  tags: ['mongoose', 'mongodb', 'nodejs']
});

Practice it.

Create a schema for a User collection where each user has a name, email, password, and age:

Fact about Mongoose

In Mongoose, every document created in a collection automatically receives a unique identifier known as _id. This identifier is crucial for managing and referencing documents within the MongoDB database.

When Ram created his account, Mongoose automatically gave him a unique ID, ObjectId("647e1f9d29b21c5e31c58909"), so the application could identify him.

Can we create our own ID in Mongoose?

Yes, you can create your own ID in Mongoose. By default, Mongoose generates a unique ObjectId for each document, but you can specify your own ID by defining a custom _id field in your schema.

Summary

Mongoose schemas define the structure and format of documents in a MongoDB collection.

  • SchemaTypes specify the data type and rules for each field, ensuring data consistency and validity.

  • Common SchemaType options include required, default, unique, enum, and validate.

  • ObjectId is used to create relationships between documents, similar to foreign keys in relational databases.

  • Indexes improve query performance by allowing faster data retrieval.

  • Mongoose supports various data types like strings, numbers, dates, maps, buffers, and arrays, each with specific options for customization.

  • Understanding and using Mongoose schemas effectively is key to building efficient and scalable applications.

0
Subscribe to my newsletter

Read articles from Priyanshu Pandey directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Priyanshu Pandey
Priyanshu Pandey