Mongoose and Schema Design
What is Mongoose?
Mongoose is an ODM (Object Document Mapper) for MongoDB and Node.js. It sits between your application code and the MongoDB database, giving you a structured way to define what your data looks like, validate it, and interact with it.
Think of Mongoose as a strict translator. MongoDB itself accepts any document shape you throw at it. Mongoose enforces rules: "a user document must have a name and a unique email, the age must be a positive number, and the role must be either 'user' or 'admin'."
Without Mongoose, nothing stops you from accidentally saving { name: 42 } as a user. With Mongoose, that would fail validation and return a clear error message.
Defining a Schema
A schema is a blueprint that describes the shape of documents in a collection. You define it once, then Mongoose enforces it on every save.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
minlength: [2, 'Name must be at least 2 characters'],
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
},
age: {
type: Number,
min: [0, 'Age cannot be negative'],
max: [120, 'Age seems too high'],
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user',
},
isActive: {
type: Boolean,
default: true,
},
createdAt: {
type: Date,
default: Date.now,
},
});
Common Field Types
| Type | Example use |
|---|---|
String | Name, email, title |
Number | Age, price, quantity |
Boolean | isActive, isVerified |
Date | createdAt, publishedAt |
Array | Tags, skills, images |
ObjectId | References to other documents |
Mixed | Any value (use sparingly) |
Creating a Model
A model is a class that Mongoose creates from your schema. You use the model to create, read, update, and delete documents in the collection.
// The first argument is the model name (singular, capitalised)
// Mongoose automatically creates a collection called 'users' (lowercase, plural)
const User = mongoose.model('User', userSchema);
module.exports = User;
Validation
Mongoose runs validation before saving any document. If a required field is missing or a value fails a rule, Mongoose throws a ValidationError with a helpful message.
try {
const user = await User.create({ name: 'A', email: 'not-an-email' });
} catch (error) {
console.log(error.message);
// "user validation failed: name: Name must be at least 2 characters"
}
You can also write custom validators:
phone: {
type: String,
validate: {
validator: function(v) {
return /^\+?[1-9]\d{7,14}$/.test(v);
},
message: props => props.value + ' is not a valid phone number',
},
},
Schema Methods and Statics
You can attach custom functions directly to your schema.
Instance Methods
Instance methods are available on individual document instances:
// Add a method to get the user's full display name
userSchema.methods.getDisplayName = function () {
return this.isActive ? this.name : this.name + ' (inactive)';
};
// Usage:
const user = await User.findOne({ email: 'ngozi@example.com' });
console.log(user.getDisplayName()); // "Ngozi (inactive)" or "Ngozi"
Static Methods
Static methods are called on the model itself, not on individual documents:
// Find all active users
userSchema.statics.findActive = function () {
return this.find({ isActive: true });
};
// Usage:
const activeUsers = await User.findActive();
Virtual Fields
Virtuals are computed properties that are not stored in the database but can be accessed like real fields.
const productSchema = new mongoose.Schema({
priceKobo: { type: Number, required: true }, // Store price in kobo (smallest unit)
});
// Virtual: convert kobo to naira for display
productSchema.virtual('priceNaira').get(function () {
return this.priceKobo / 100;
});
const Product = mongoose.model('Product', productSchema);
const product = await Product.findOne({ ... });
console.log(product.priceKobo); // 250000
console.log(product.priceNaira); // 2500 — calculated, not stored
To include virtuals when converting to JSON (e.g. in API responses), enable them in your schema options:
const productSchema = new mongoose.Schema(
{ priceKobo: Number },
{ toJSON: { virtuals: true }, toObject: { virtuals: true } }
);
Timestamps
Instead of manually managing createdAt and updatedAt, Mongoose can do it automatically:
const postSchema = new mongoose.Schema(
{
title: String,
body: String,
},
{ timestamps: true } // Automatically adds createdAt and updatedAt
);
Every time you create or update a document, Mongoose updates these fields for you.
A Complete Schema Example
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, 'Product name is required'],
trim: true,
},
description: String,
priceKobo: {
type: Number,
required: true,
min: [0, 'Price cannot be negative'],
},
category: {
type: String,
enum: ['electronics', 'clothing', 'food', 'books'],
required: true,
},
stock: {
type: Number,
default: 0,
},
isAvailable: {
type: Boolean,
default: true,
},
seller: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // Reference to the User model
required: true,
},
},
{ timestamps: true, toJSON: { virtuals: true } }
);
productSchema.virtual('priceNaira').get(function () {
return this.priceKobo / 100;
});
productSchema.statics.findByCategory = function (category) {
return this.find({ category, isAvailable: true });
};
module.exports = mongoose.model('Product', productSchema);
Practice Exercise
Design a Mongoose schema for a job listing platform:
- Create a
Jobmodel with fields:title(required string),company(required string),location(string),type(enum of "full-time", "part-time", "contract", "remote"),salaryMinandsalaryMax(numbers),isOpen(boolean, default true), and a reference to aUseraspostedBy - Add a virtual
salaryRangethat returns a formatted string like "₦200,000 — ₦350,000" - Add a static method
findOpenJobsthat returns all jobs whereisOpenis true - Add a custom validator that ensures
salaryMaxis greater thansalaryMin - Enable timestamps and test by creating a job document
Try it yourself
Key Takeaways
- Mongoose is an ODM that enforces structure, validation, and rules on top of MongoDB's flexible document model.
- A schema defines the shape, types, required fields, defaults, and validation rules for documents in a collection.
- A model is created from a schema and provides the interface for creating, reading, updating, and deleting documents.
- Instance methods are attached to individual documents; static methods are attached to the model class for reusable queries.
- Virtual fields are computed properties not stored in the database — ideal for derived values like formatted prices or display names.
Quick Quiz
1.What does ODM stand for in the context of Mongoose?
2.What is the difference between a Mongoose schema method and a static method?
3.What is a Mongoose virtual field?
4.What Mongoose schema option automatically manages createdAt and updatedAt fields?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx