DocsHub
Mongoose

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:

SchemaTypeJavaScript equivalentExample
Stringstring"Ali Hassan"
Numbernumber16, 3.75
Booleanbooleantrue, false
DateDate objectnew Date("2024-09-01")
ObjectIdMongoDB ObjectIdnew mongoose.Types.ObjectId()
Arrayarray["Math", "Physics"]
Mixedanythingany value, no type checking
Mapkey-value mapMap { "key" => "value" }
Bufferbinary datafor 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 now

timestamps: 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 uppercase
const 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

ConceptSyntaxPurpose
Basic typename: StringDefine 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.

On this page