Schema and Model
Connecting backend application to the MongoDB is the first step, and to do database operations, we need to define a schema and model. In this documentation, we'll learn how to create a schema and model using mongoose.
Let's start by learning what is a schema in general, and then we'll learn how to create a schema and model using mongoose.
What is a schema?
A schema is a blueprint of the data. It defines the structure of the data and also the validation rules for the data.
For example, check out the following data:
{
"name": "Rizwan Ashiq",
"age": 30,
"email": "email@example.com"
}
The above data has three fields: name, age, and email. The name field is a String, the age field is a Number, and the email field is a String.
Now, let's create a schema for the above data.
Creating a schema
For now, let's simply create a schema for the above data without any validation rules, just mention the data type of each field.
const schema = {
name: String,
age: Number,
email: String,
};
Using mongoose, we create a schema by passing the schema object to the mongoose.Schema() constructor.
Let's create a schema for the above data:
import mongoose from "mongoose";
// Define the schema
const schema = {
name: String,
age: Number,
email: String,
};
// Create `mongoose` schema from the schema object
const PersonSchema = new mongoose.Schema(schema);
The PersonSchema is a Mongoose schema. Simple, isn't it? ☺️
There's a website called https://transform.tools/json-to-mongoose that can help you convert JSON to mongoose schema. You can paste your JSON data, and it will generate a simple schema for you.
You can also define nested objects in the schema. For example in the above example, the address field is defined as a String type. You can also specify the address field as an object with the following structure:
{
"name": "Rizwan Ashiq",
"age": 30,
"email": "email@example.com",
"address": {
"street": "123 Main St",
"city": "Rahim Yar Khan",
"state": "Punjab",
"zip": 64200,
"country": "Pakistan"
}
}
The schema for the above data would be:
const PersonSchema = new mongoose.Schema({
name: String,
age: Number,
email: String,
address: {
street: String,
city: String,
state: String,
zip: Number,
country: String,
},
});
In the above schema, I just mentioned the type of each field. But, if we want to add more options to the field like required, default, etc.
Then, we'll do it like this:
const PersonSchema = new mongoose.Schema({
name: { type: String, required: true },
age: { type: Number, required: true },
email: { type: String, required: true },
address: {
street: String,
city: String,
state: String,
zip: { type: Number },
country: { type: String, default: "Pakistan" },
},
});
Check the schema, you'll notice that I added the required and default options to the fields. If there's just type, then you can simply mention the type of the field. But, if you want to add more options, then you'll have to mention the field as an object and add the options to it.
Data validation
mongoose provides two ways to validate the data:
Most of the time, we'll use built-in validation. But, if there's a complex validation rule, then you can use custom validation.
Built-in validation
mongoose provides a wide range of built-in validation rules. You can check the official documentation for all the available validation rules.
Let me show you some of the most commonly used validation rules.
| Validation rule | Description | Example |
|---|---|---|
required | The field is required. | name: { type: String, required: true } |
default | The default value for the field. | country: { type: String, default: "Pakistan" } |
min | The minimum value for the field. | age: { type: Number, min: 0 } |
max | The maximum value for the field. | age: { type: Number, max: 120 } |
index | Create an index for the field. | name: { type: String, index: true } |
unique | The field value must be unique, and will value with the existing documents in the collection | email: { type: String, unique: true } |
match | The field value must match the given regex. | email: { type: String, match: /\S+@\S+\.\S+/ } |
Let's add some validation rules to the PersonSchema:
const PersonSchema = new mongoose.Schema({
name: { type: String, required: true, index: true }, // index: true will create an index for this field
age: { type: Number, min: 0, max: 120 },
email: { type: String, match: /\S+@\S+\.\S+/ }, // email regex
phone: { type: String, match: /^\+92-\d{3}-\d{3}-\d{4}$/ }, // regex +92-3**-***-****
isAlive: { type: Boolean, default: true },
gender: { type: String, enum: ["male", "female", "other"] },
photosURLs: { type: [String] },
notes: { type: String },
address: {
street: {
type: String,
},
city: {
type: String,
capitalize: true, // will capitalize the first letter of the string
},
state: {
type: String,
uppercase: true, // will convert the string to uppercase,
},
zip: {
type: String,
length: 5, // will make sure the string length is 5
},
country: {
type: String,
lowercase: true, // will convert the string to lowercase
trim: true, // will remove the leading and trailing spaces
},
},
});
You can see that I added different validation rules to the fields. For example, I added index: true to the name field, which will create an index for the name field. Similarly, I added min: 0 and max: 120 to the age field, which will make sure that the age value is between 0 and 120.
I also added a regex to the email field, which will make sure that the email value is a valid email address. Similarly, I added a regex to the phone field, which will make sure that the phone value is a valid phone number.
What's regex?
Regex (short for "regular expression") is a pattern that describes a set of strings. It is a sequence of characters that defines a search pattern, which can be used to match or manipulate strings in various programming languages and applications. Regular expressions are a powerful tool for pattern matching and data validation.
If you didn't understand the regex, no worries, it's not important for now. Ignore it, and just focus on the mongoose part.
Custom validation
Custom validation is also possible. It may look a bit complicated at first, just read it, and no worries if you don't understand it. Normally we don't use custom validations. I am writing this just to show you that it's possible. Here's an example of a schema with custom validation:
const PersonSchema = new mongoose.Schema({
// ...
password: {
type: String,
required: true,
},
confirmPassword: {
type: String,
required: true,
validate: {
validator: function (value) { return value === this.password; }, // must use regular function — arrow functions don't bind `this`
message: "Confirm password must match password",
},
},
});
In the above example, we have defined a schema for a person. The password field is a string, and it is required. The confirmPassword field is a string, and it is required. The confirmPassword field must match the password field. If it doesn't match, the message will be shown.
Creating model
First of all, what's a model?
What is a model?
A model is a class with which we construct documents. In other words, a model is an interface between the application and the database for creating, querying, updating, deleting records, etc.
Let's Create
To start creating documents based on the Person schema, we need to compile our schema into a model. We can do this by using the model() method:
import mongoose from "mongoose";
const PersonSchema = new mongoose.Schema({
name: { type: String, required: true, index: true },
age: { type: Number, min: 0, max: 120 },
email: { type: String, match: /\S+@\S+\.\S+/ }, // email regex
phone: { type: String, match: /^\+92-\d{3}-\d{3}-\d{4}$/ }, // regex +92-3**-***-****
isAlive: { type: Boolean, default: true },
gender: { type: String, enum: ["male", "female", "other"] },
photosURLs: { type: [String] },
notes: { type: String },
});
// compiling schema into a model
const Person = mongoose.model("Person", PersonSchema);
// exporting Model to use it in other files
export default Person;
The model() method takes two arguments.
- The first argument is the name of the model. It's the name you use to reference the model in your code. It should be a singular name. I will explain the difference between the model name and the collection name later.
- The second argument is the schema you want to use in creating the model.
The model() method makes a copy of all we defined on the schema. It also contains all the mongoose methods we will use to interact with the database. If we want to use this model outside the model's file. We need to export it.
Difference between model name and collection name
Model Name
When you define a mongoose model using mongoose.model('Name', schema), the first argument 'Name' represents the singular name of the model. It's the name you use to reference the mongoose model in your code. For example:
import mongoose from "mongoose";
const Schema = mongoose.Schema;
const userSchema = new Schema({
name: String,
email: String,
});
const UserModel = mongoose.model("User", userSchema);
In this example, User is the model name, it's the name you use to reference the mongoose model in your code.
Collection Name
By default, mongoose automatically pluralizes the model name to determine the collection name in MongoDB. For example, if your model name is User, mongoose will create a collection called users in the MongoDB database.
You can also explicitly specify a different collection name by passing a third argument to the mongoose.model function:
const User = mongoose.model("User", userSchema, "custom_users");
In this example, the collection name will be 'custom_users' in the MongoDB database, regardless of the model name being 'User'.
Reference to Other Models
We can expect that all middle-sized applications would have more than one collection, and possibly those could be linked in some way. This means they may have some fields that reference other documents in other collections. For example, a person document may have a mother and father field that references other documents in the people collection. Or a post document may have an author field that references a document in the users collection.
In our example, to represent a family tree we need to add a mother, and father, and I want to class_id field as well to reference the class the person is in.
mother, and father fields will reference other documents in the same collection, and class_id will reference a document in the Class collection. All of these fields will be of type ObjectId.
First of all, let's create a Class model:
import mongoose from "mongoose";
const ClassSchema = new mongoose.Schema({
name: { type: String, required: true, index: true },
year: { type: Number, required: true },
});
const Class = mongoose.model("Class", ClassSchema);
export default Class;
Now, let's add the mother, father, and class_id fields to the PersonSchema:
const PersonSchema = new mongoose.Schema({
// ...
mother: { type: mongoose.Schema.Types.ObjectId, ref: "Person" },
father: { type: mongoose.Schema.Types.ObjectId, ref: "Person" },
class_id: { type: mongoose.Schema.Types.ObjectId, ref: "Class" },
// ...
});
In the above example, we have defined a schema for a person. The mother, father, and class_id fields are of type ObjectId, and they reference other documents in the same collection. The ref property must be the name of the model we are referencing. This ref will have the same name that we used in the model() method. In our case, it's Person and Class.
The downward 👇 topics are a bit advanced, and you don't need to know them right now if you are a beginner, and you are just starting with MongoDB.
You can come back to it later when you have a better understanding of MongoDB after completing the whole MongoDB section, or at least the basic part.
However, if you are an intermediate, advanced, or can't wait to learn more, then you can continue reading.
Happy learning! 😊
Hooks
Hooks are middleware functions that are called before or after events like save, validate, remove, etc. Hooks are useful for executing some code before or after some event happens on a document. In other words, hooks allow us to run some code before or after a certain event happens on a document.
There are two types of hooks:
Pre Hooks
Pre hooks are functions that get executed before a certain event happens on a document, such as saving or removing it. Pre hooks can be used to modify or validate the data before it gets saved or removed.
For example, we can use pre hooks to hash the password before saving the document to the database. Here's how we can do that:
const PersonSchema = new mongoose.Schema({
// ...
password: {
type: String,
required: true,
},
});
PersonSchema.pre("save", async function (next) {
this.password = await bcrypt.hash(this.password, 10);
next();
});
// This code will be executed before the save method, and the password will be hashed before saving it to the database.
This pre hook is executed before the save method. In the pre hook, we are hashing the password using bcrypt and then calling the next function to continue the execution of the save method.
Post Hooks
Post hooks are functions that get executed after a certain event happens on a document, such as saving or removing it. Post hooks can be used to perform additional actions after the document has been saved or removed.
For example, we can use post hooks to send an email after saving the document to the database. Here's how we can do that:
const PersonSchema = new mongoose.Schema({
// ...
email: {
type: String,
required: true,
},
});
PersonSchema.post("save", async function (doc) {
await sendEmail(doc.email, "Welcome to our family"); // Need to define sendEmail function somewhere
});
This post hook is executed after the save method. In the post hook, we are sending an email to the person.
Static Methods
Static methods are defined on the model itself and can be used without creating an instance of the model. These methods are useful when you want to operate on the entire collection of documents or when you need to perform an operation that doesn't require any specific document instance.
Example:
Suppose we want to find all the people who are older than 18 years old. We can define a static method to do that. Here's how we can do that:
const PersonSchema = new mongoose.Schema({
// ...
age: {
type: Number,
required: true,
min: 0,
},
});
PersonSchema.statics.findAdults = function () {
return this.find({ age: { $gte: 18 } });
};
const Person = mongoose.model("Person", PersonSchema);
const adults = await Person.findAdults();
The findAdults static method is used to find all the people who are older than 18 years old. The findAdults static method is defined on the PersonSchema statics object. The findAdults static method is called on the Person model.
Instance Methods
Instance methods are defined on the model's schema and can be used on instances of the model. These methods are useful when you want to perform an operation on a specific document instance.
Example:
Suppose we want to check if a person is older than 18 years old. We can define an instance method to do that. Here's how we can do that:
const PersonSchema = new mongoose.Schema({
// ...
age: {
type: Number,
required: true,
min: 0,
},
});
PersonSchema.methods.isAdult = function () {
return this.age >= 18;
};
const Person = mongoose.model("Person", PersonSchema);
const person = new Person({ age: 20 });
const isAdult = person.isAdult();
console.log(isAdult); // true
The isAdult instance method is used to check if a person is older than 18 years old. The isAdult instance method is defined on the PersonSchema methods object. The isAdult instance method is called on a Person document.
In short, static methods do operations on the entire collection of documents, and instance methods do operations on a specific document instance.
Conclusion
In this section, we learned about schemas, hooks, static methods, and instance methods.
We'll see more about mongoose in the next docs.