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,ObjectIdand 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 arelowercase,minlength,maxlengthandmatch(a regular expression). - `enum` lists the allowed values. Our
statuscan 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
- Create
server/models/Job.jsfrom the lesson and runnode scripts/model-demo.js. - Add a
urlfield (string, optional, must start withhttp://orhttps://usingmatch). Test it with a good and a bad value in the demo script. - Add a
sourcefield withenum: ['website', 'referral', 'agency', 'other']and a default ofother. - Make
appliedOndefault to nothing, and write down why a default ofDate.nowwould be wrong for a job with statussaved.
How to check it worked
- The demo prints one message per broken rule.
- The good job is trimmed and has
status: 'saved'and anid.
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.
toJSONdecides what leaves the server, so hide internals there.