Schemas
Learn how to define Mongoose schemas, use SchemaTypes, set default values, and configure schema options like timestamps.
Schemas
A schema is the blueprint for your documents. It defines what fields exist, what type each field is, and rules each field must follow. Every model in Mongoose is built from a schema.
This file covers how to define schemas properly — the foundation for everything else in this section.
Defining a Schema
const mongoose = require('mongoose');
const studentSchema = new mongoose.Schema({
name: String,
age: Number,
grade: String,
enrolled: Boolean
});Each key is a field name. Each value is the type for that field. This is the shorthand syntax — Mongoose also supports a more detailed object syntax which we will use throughout this guide.
SchemaTypes
Mongoose supports these field types:
| SchemaType | JavaScript equivalent | Example |
|---|---|---|
String | string | "Ali Hassan" |
Number | number | 16, 3.75 |
Boolean | boolean | true, false |
Date | Date object | new Date("2024-09-01") |
ObjectId | MongoDB ObjectId | new mongoose.Types.ObjectId() |
Array | array | ["Math", "Physics"] |
Mixed | anything | any value, no type checking |
Map | key-value map | Map { "key" => "value" } |
Buffer | binary data | for files, images |
Basic types
const studentSchema = new mongoose.Schema({
name: String,
age: Number,
enrolled: Boolean,
enrollmentDate: Date
});ObjectId — references to other documents
const courseSchema = new mongoose.Schema({
title: String,
code: String,
teacherId: mongoose.Schema.Types.ObjectId
});We will use ObjectId heavily in the Population file — it is how Mongoose references documents in other collections.
Arrays
const studentSchema = new mongoose.Schema({
name: String,
subjects: [String] // array of strings
});Array of objects (subdocuments)
const studentSchema = new mongoose.Schema({
name: String,
grades: [
{
subject: String,
score: Number,
semester: Number
}
]
});Each element in grades is itself a small embedded document with subject, score, and semester.
Embedded objects
const studentSchema = new mongoose.Schema({
name: String,
address: {
city: String,
country: String
}
});Mixed type
Mixed allows any type — no validation. Use it sparingly, only when a field's structure is genuinely unpredictable.
const studentSchema = new mongoose.Schema({
name: String,
customAttributes: mongoose.Schema.Types.Mixed
});// Both of these are valid with Mixed
{ customAttributes: { sport: "Cricket" } }
{ customAttributes: "any string value" }Avoid Mixed unless absolutely necessary. It disables Mongoose's type checking and validation for that field — you lose one of the main benefits of using Mongoose. Most "unpredictable" data can still be modeled with a defined structure.
The Detailed Field Syntax
Instead of just specifying a type, you can specify an object with the type plus options:
const studentSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
age: {
type: Number,
required: true,
min: 5,
max: 25
},
grade: {
type: String,
enum: ["9th", "10th", "11th", "12th"],
required: true
},
enrolled: {
type: Boolean,
default: true
}
});This is the syntax you will use most. We cover validation options like required, min, max, and enum in detail in the Validation file. For now, focus on understanding the structure.
Default Values
Use default to set a value automatically when a field is not provided:
const studentSchema = new mongoose.Schema({
name: String,
enrolled: {
type: Boolean,
default: true
},
enrollmentDate: {
type: Date,
default: Date.now // function — runs when document is created
},
hasPaidFees: {
type: Boolean,
default: false
}
});// You don't need to provide enrolled, enrollmentDate, or hasPaidFees
const student = await Student.create({
name: "Ali Hassan",
age: 16,
grade: "10th"
});
console.log(student.enrolled); // true (default)
console.log(student.enrollmentDate); // current date (default)
console.log(student.hasPaidFees); // false (default)Notice default: Date.now — without parentheses. This passes the function itself, which Mongoose calls when the document is created. If you write Date.now() with parentheses, the date is calculated once when the schema is defined — every document would get the same date.
Schema Options
The second argument to new mongoose.Schema() is an options object that controls schema-wide behavior.
const studentSchema = new mongoose.Schema(
{
name: String,
age: Number,
grade: String
},
{
timestamps: true
}
);timestamps
Automatically adds createdAt and updatedAt fields, managed by Mongoose:
const studentSchema = new mongoose.Schema(
{
name: String,
age: Number
},
{ timestamps: true }
);
const Student = mongoose.model('Student', studentSchema);
const student = await Student.create({ name: "Ali Hassan", age: 16 });
console.log(student.createdAt); // set automatically
console.log(student.updatedAt); // set automatically
// updatedAt changes automatically on save
student.age = 17;
await student.save();
console.log(student.updatedAt); // updated to nowtimestamps: true is one of the most commonly used schema options — almost every collection benefits from knowing when a document was created and last updated.
collection
By default, Mongoose creates a collection name by lowercasing and pluralizing the model name — Student becomes students. Override this with the collection option:
const studentSchema = new mongoose.Schema(
{ name: String },
{ collection: 'enrolled_students' } // custom collection name
);Building Our School System Schemas
Let's define the four core schemas for our school system. We will keep them simple here — validation, middleware, and other features come in later files.
Student Schema
const mongoose = require('mongoose');
const studentSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true
},
age: {
type: Number,
required: true,
min: 5,
max: 25
},
grade: {
type: String,
enum: ["9th", "10th", "11th", "12th"],
required: true
},
enrolled: {
type: Boolean,
default: true
},
enrollmentDate: {
type: Date,
default: Date.now
},
address: {
city: String,
country: String
},
guardian: {
name: String,
phone: String,
relation: {
type: String,
enum: ["Father", "Mother", "Brother", "Sister", "Uncle", "Aunt", "Other"]
}
},
subjects: [String],
grades: [
{
subject: String,
score: { type: Number, min: 0, max: 100 },
semester: { type: Number, enum: [1, 2] }
}
],
courseIds: [
{ type: mongoose.Schema.Types.ObjectId, ref: "Course" }
]
},
{ timestamps: true }
);
module.exports = mongoose.model('Student', studentSchema);Teacher Schema
const mongoose = require('mongoose');
const teacherSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true
},
subject: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
active: {
type: Boolean,
default: true
},
joinedDate: {
type: Date,
default: Date.now
}
},
{ timestamps: true }
);
module.exports = mongoose.model('Teacher', teacherSchema);Course Schema
const mongoose = require('mongoose');
const courseSchema = new mongoose.Schema(
{
title: {
type: String,
required: true
},
code: {
type: String,
required: true,
unique: true,
uppercase: true
},
grade: {
type: String,
enum: ["9th", "10th", "11th", "12th"],
required: true
},
credits: {
type: Number,
default: 4
},
teacherId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Teacher"
},
totalStudents: {
type: Number,
default: 0
},
capacity: {
type: Number,
default: 30
}
},
{ timestamps: true }
);
module.exports = mongoose.model('Course', courseSchema);Enrollment Schema
const mongoose = require('mongoose');
const enrollmentSchema = new mongoose.Schema(
{
studentId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Student",
required: true
},
courseId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Course",
required: true
},
enrolledAt: {
type: Date,
default: Date.now
},
status: {
type: String,
enum: ["active", "completed", "dropped"],
default: "active"
},
grade: {
type: String,
default: null
}
},
{ timestamps: true }
);
module.exports = mongoose.model('Enrollment', enrollmentSchema);String Transformation Options
Notice trim, lowercase, and uppercase in the schemas above. These are automatic transformations Mongoose applies before saving:
name: { type: String, trim: true } // removes whitespace from both ends
email: { type: String, lowercase: true } // converts to lowercase
code: { type: String, uppercase: true } // converts to uppercaseconst teacher = await Teacher.create({
name: " Mr. Khan ", // saved as "Mr. Khan"
email: "KHAN@SCHOOL.COM", // saved as "khan@school.com"
});
const course = await Course.create({
title: "Mathematics",
code: "math-10" // saved as "MATH-10"
});These transformations happen automatically — no extra code needed in your application logic.
Quick Reference
| Concept | Syntax | Purpose |
|---|---|---|
| Basic type | name: String | Define field type |
| Detailed type | { type: String, required: true } | Type plus options |
| Default value | { default: true } or { default: Date.now } | Auto-fill if not provided |
| Array | [String] or [{ ... }] | List of values or subdocuments |
| Reference | { type: ObjectId, ref: "ModelName" } | Link to another collection |
| timestamps | { timestamps: true } | Auto createdAt / updatedAt |
| trim / lowercase / uppercase | { trim: true } | Auto string transformation |
Define your schemas thoughtfully from the start — they are the foundation of validation, population, and middleware that we build on in the next files. Use the detailed field syntax even for simple fields. It costs a few extra characters now and saves you from rewrites later when you need to add required, default, or other options.