Débutant25 min de lecture

Schemas & Models

Define document structure with Mongoose schemas, create models, and understand data validation.

Mongoose Schemas

A schema defines the shape of documents within a MongoDB collection. While MongoDB itself does not require any structure, Mongoose schemas let you enforce structure at the application level. This is one of the biggest reasons developers choose Mongoose over the native driver — you get the flexibility of NoSQL with the predictability of defined data structures.

A schema maps directly to a MongoDB collection. When you define a User schema, every document created through that schema will conform to the rules you set. Think of a schema as a blueprint or a contract: it specifies what fields a document can have, what types those fields must be, and what constraints they must satisfy.

Schema Types — Mongoose supports the following types for schema fields:

  • String — text values
  • Number — integers and floating-point numbers
  • Boolean — true or false
  • Date — JavaScript Date objects
  • Array — arrays of values (can be typed: [String] for an array of strings)
  • ObjectId — MongoDB ObjectId, used for referencing other documents
  • Mixed — any type (opt-out of type checking for this field)
  • Buffer — binary data
  • Map — key-value pairs with typed values
  • Schema.Types.Decimal128 — high-precision decimal numbers

Each field in your schema can have options that add validation and behavior:

  • required: true — the field must have a value (cannot be null/undefined)
  • unique: true — no two documents can have the same value for this field
  • default: value — automatically assign this value if none is provided
  • min: 0 / max: 150 — minimum/maximum for Numbers
  • minlength: 2 / maxlength: 100 — minimum/maximum length for Strings
  • enum: ['user', 'admin'] — value must be one of these options
  • trim: true — remove whitespace from beginning/end of Strings
  • lowercase: true / uppercase: true — convert String case automatically
  • match: /regex/ — String must match this regular expression
  • validate: { validator: fn, message: 'Error message' } — custom validation

Defining a Complete User Schema

javascript
const mongoose = require('mongoose');

// Define the User schema with types, validation, and defaults
const userSchema = new mongoose.Schema({
  // Required string with trimming and length limits
  name: {
    type: String,
    required: [true, 'Name is required'],  // Custom error message
    trim: true,
    minlength: [2, 'Name must be at least 2 characters'],
    maxlength: [50, 'Name cannot exceed 50 characters']
  },

  // Required unique email with lowercase conversion
  email: {
    type: String,
    required: [true, 'Email is required'],
    unique: true,         // No two users can share an email
    lowercase: true,      // Automatically convert to lowercase
    trim: true,
    match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email']
  },

  // Optional number with min/max range
  age: {
    type: Number,
    min: [0, 'Age cannot be negative'],
    max: [150, 'Age must be realistic']
  },

  // Enum field with a default value
  role: {
    type: String,
    enum: {
      values: ['user', 'admin', 'moderator'],
      message: '{VALUE} is not a valid role'
    },
    default: 'user'
  },

  // Array of strings
  hobbies: [String],

  // Nested object (sub-document)
  address: {
    street: String,
    city: String,
    country: { type: String, default: 'Unknown' }
  },

  // Auto-set creation date
  createdAt: {
    type: Date,
    default: Date.now    // Function reference, called when doc is created
  }
});

// Create the model from the schema
const User = mongoose.model('User', userSchema);

Mongoose Models

A model is a constructor function that Mongoose compiles from a schema. It is the primary interface you use to interact with a MongoDB collection. While a schema defines the shape and rules for documents, a model provides the actual methods for creating, reading, updating, and deleting documents in the database.

You create a model by passing a name and a schema to mongoose.model():

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

The first argument ('User') is the singular name of the collection. Mongoose automatically creates a collection with the pluralized, lowercased version of this name. So 'User' maps to a users collection, 'Post' maps to posts, 'BlogComment' maps to blogcomments. This convention is automatic, but you can override it in the schema options if needed.

Once you have a model, you can create new documents from it. A model instance (a single document) is called a document. Creating a document is like calling new on a constructor:

javascript
const alice = new User({ name: 'Alice', email: 'alice@example.com' });
await alice.save(); // Saves to the database

// Or use the shorthand:
const bob = await User.create({ name: 'Bob', email: 'bob@example.com' });

The model also provides static methods for querying the entire collection: User.find(), User.findById(), User.findOne(), User.findByIdAndUpdate(), User.findByIdAndDelete(), User.countDocuments(), and many more. These are the workhorses of your data layer.

Important convention: model names are always singular and PascalCase (User, BlogPost, OrderItem). The corresponding collection names are automatically plural and lowercase (users, blogposts, orderitems). Keep your models in separate files: models/User.js, models/Post.js, etc.

Schema Validation

Validation is one of the most powerful features Mongoose adds on top of MongoDB. It ensures that bad data never reaches your database. Mongoose runs validators automatically when you call save() or create(). If any validator fails, the operation throws a ValidationError with details about what went wrong.

Built-in Validators:

  • required — Works on all types. The field must have a value. Empty strings, null, and undefined all fail.
  • min / max — Numbers only. Ensures the value falls within a range: age: { type: Number, min: 0, max: 150 }.
  • minlength / maxlength — Strings only. Controls string length: name: { type: String, minlength: 2, maxlength: 100 }.
  • enum — Strings only. Value must be in a whitelist: role: { type: String, enum: ['user', 'admin'] }.
  • match — Strings only. Value must match a regular expression: email: { type: String, match: /^\S+@\S+\.\S+$/ }.

Custom Validators:

For rules that built-in validators cannot express, you write custom validator functions:

javascript
phone: {
  type: String,
  validate: {
    validator: function(value) {
      return /^\d{10}$/.test(value);  // Must be exactly 10 digits
    },
    message: 'Phone number must be 10 digits'
  }
}

Handling Validation Errors:

When validation fails, Mongoose throws an error with an errors object. Each key is the field name, and each value contains the error message and other details. You typically catch this in a try/catch block and return a 400 Bad Request response to the client with the validation details. Always validate on the server side even if you also validate on the frontend — client-side validation can be bypassed, but server-side validation backed by Mongoose cannot.

Schema Options & Methods

Mongoose schemas support several powerful options and customization features that go beyond basic field definitions.

Timestamps — The most commonly used schema option. Adding { timestamps: true } as the second argument to new mongoose.Schema() automatically creates and maintains two fields: createdAt (set when the document is first saved) and updatedAt (updated every time the document is modified). This saves you from manually managing these fields:

javascript
const postSchema = new mongoose.Schema({ title: String, body: String }, { timestamps: true });
// Documents now automatically have createdAt and updatedAt

Instance Methods — Custom methods available on individual document instances. You define them on schema.methods. They have access to the document via this:

javascript
userSchema.methods.getPublicProfile = function() {
  return { name: this.name, email: this.email };
  // Excludes sensitive fields like password
};
const user = await User.findById(id);
const profile = user.getPublicProfile();

Static Methods — Custom methods on the model itself (not on individual documents). Useful for custom queries:

javascript
userSchema.statics.findByEmail = function(email) {
  return this.findOne({ email: email.toLowerCase() });
};
const user = await User.findByEmail('ALICE@example.com');

Virtual Properties — Computed fields that are not stored in the database. They are calculated on-the-fly from existing fields:

javascript
userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});
// user.fullName returns 'Alice Johnson' but is not in the DB

Middleware (Hooks) — Run code before or after specific operations. The pre('save') hook is commonly used for password hashing:

javascript
userSchema.pre('save', async function(next) {
  if (this.isModified('password')) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});

These features make Mongoose much more than a simple database wrapper — it is a full-featured data modeling tool.

What does the 'required: true' option do in a Mongoose schema?

Prêt à pratiquer ?

Crée ton compte gratuit pour accéder à l'éditeur de code interactif, lancer les défis et suivre ta progression.