Mongoose Schemas and Models: Types, Validation, Defaults and Timestamps

MERN Stack Full Course: MongoDB, Express, React and Node.js · MongoDB and Mongoose · 5 min read · Updated

Schema to model to documents

  1. A schema describes a Job: its fields, their types, required fields, defaults and allowed values.
  2. mongoose.model turns the schema into a model, the tool we use to read and write jobs.
  3. Creating a document with the model checks it against the schema. Bad data is rejected with a ValidationError.
  4. timestamps adds createdAt and updatedAt for free.
  5. save() writes the document into the collection.

A MongoDB collection will happily store anything. If one document has company and another has Company and a third has company: 42, MongoDB does not complain, and your React app will break in strange ways. A Mongoose schema is the rulebook that prevents this. A model is built from the schema and is the object you use to create, read, update and delete documents.

The Job schema

import mongoose from 'mongoose';

export const STATUSES = ['saved', 'applied', 'interview', 'offer', 'rejected'];

// A schema describes the shape of a document and the rules it must follow.
const jobSchema = new mongoose.Schema(
  {
    company: { type: String, required: [true, 'company is required'], trim: true, maxlength: 120 },
    role: { type: String, required: [true, 'role is required'], trim: true, maxlength: 120 },
    location: { type: String, trim: true, maxlength: 120, default: '' },
    status: { type: String, enum: { values: STATUSES, message: 'status must be one of: ' + STATUSES.join(', ') }, default: 'saved' },
    salaryNote: { type: String, trim: true, maxlength: 120, default: '' }, // free text on purpose: pay is written many ways
    notes: { type: String, trim: true, maxlength: 2000, default: '' },
    appliedOn: { type: Date },
  },
  { timestamps: true }, // adds createdAt and updatedAt for free
);

// Send `id` instead of `_id` and hide the internal version field.
jobSchema.set('toJSON', {
  virtuals: true,
  versionKey: false,
  transform: (doc, ret) => { delete ret._id; return ret; },
});

export default mongoose.model('Job', jobSchema);

Walk through it field by field.

  • `type` converts values to that type when possible and rejects them when not. String, Number, Date, Boolean, ObjectId and arrays are the ones you will use.
  • `required: [true, 'message']` fails validation when the field is missing or empty. The second item is the error message the client will see.
  • `trim: true` removes spaces at both ends before saving, so ' Acme ' is stored as 'Acme'. Other helpful string options are lowercase, minlength, maxlength and match (a regular expression).
  • `enum` lists the allowed values. Our status can only be one of five words. Anything else is rejected before it reaches the database.
  • `default` fills a value when none is given. A new job starts as saved.

salaryNote is a string on purpose. As lesson 6 explained, advertised pay is written in many ways and converting it would lose information or show wrong numbers.

Timestamps and the toJSON transform

{ timestamps: true } makes Mongoose maintain createdAt and updatedAt for you. You will sort by createdAt in lesson 18, and the React app uses updatedAt in lesson 31.

The toJSON setting controls what res.json(job) sends. virtuals: true adds id, a text version of _id, which is friendlier to React. The transform then deletes _id and __v (a version counter used internally). Shaping output in one place means no route can forget to hide something. In lesson 20 the same trick will hide password hashes, which is the reason it is worth learning now.

Using the model without a server

You do not need Express to try a model. new Job({...}) builds a document in memory, validateSync() checks the rules and returns the problems without touching the database, and toJSON() shows what would be sent. The script below does exactly that.

// Shows validation and defaults without starting the web server.   Run:  node scripts/model-demo.js
import 'dotenv/config';
import mongoose from 'mongoose';
import Job from '../models/Job.js';

const bad = new Job({ company: '', status: 'maybe' });
const error = bad.validateSync(); // checks the rules without touching the database
console.log('Validation problems:');
for (const [field, e] of Object.entries(error.errors)) console.log(` - ${field}: ${e.message}`);

const good = new Job({ company: '  Acme Logistics  ', role: 'Dispatcher' });
console.log('\nTrimmed and defaulted:', good.toJSON());
await mongoose.disconnect();

Run it with node scripts/model-demo.js from the server folder. You will see two validation messages (company and role are required, status is not allowed) and then a good job with its trimmed company and the default status. This is the same check that will run automatically every time a route creates or updates a job.

Your task

  1. Create server/models/Job.js from the lesson and run node scripts/model-demo.js.
  2. Add a url field (string, optional, must start with http:// or https:// using match). Test it with a good and a bad value in the demo script.
  3. Add a source field with enum: ['website', 'referral', 'agency', 'other'] and a default of other.
  4. Make appliedOn default to nothing, and write down why a default of Date.now would be wrong for a job with status saved.

How to check it worked

  • The demo prints one message per broken rule.
  • The good job is trimmed and has status: 'saved' and an id.

Summary

  • A schema defines types, rules and defaults; a model is what you query with.
  • Validation messages from the schema become the field errors the client shows.
  • toJSON decides what leaves the server, so hide internals there.

Download the solution (mern-lesson-16.zip)

Frequently asked questions

What is the difference between a schema and a model?

The schema describes the shape and rules of a document. The model is created from a schema with mongoose.model() and gives you the methods to work with a collection: create, find, update, delete.

Does MongoDB itself enforce my schema?

No. The schema is enforced by Mongoose in your application code. MongoDB also has its own optional validation rules, but Mongoose validation is the usual first line of defence. Anything written outside Mongoose is not checked by it.

What does timestamps: true do?

It adds createdAt and updatedAt fields and keeps them up to date automatically, so you can sort by newest and know when something last changed.