W03 Learning Activity: Mongoose
Overview
- Purpose: Learn about Mongoose models and schemas.
- Task: Read the following information and complete the activity instructions below.
In Weeks 01 and 02, you worked directly with the MongoDB Node.js driver. This week, you will learn how about the Mongoose library, which adds models, schemas, validation, and helper methods on top of MongoDB.
Mongoose is an Object Data Modeling library, often called an ODM. An ODM gives JavaScript code a structured way to work with database documents. Instead of writing every database query directly against a collection, you define models that represent important resources in your application.
In an ecommerce application, Mongoose can help represent resources such as products, categories, orders, and customers. It can also help enforce rules about required fields and relationships between those resources.
Preparation Material
Why use Mongoose?
MongoDB is flexible. That flexibility is useful, but it also means your application code must be careful about document shape, required fields, and validation. Mongoose helps by letting you define a schema for each type of document your app stores.
- A schema defines the shape and rules for documents.
- A model gives your code methods for querying and changing documents in a collection.
- Mongoose can run validation before data is saved.
- Mongoose can make relationship fields more consistent through references.
Without Mongoose, your code might insert any object into a collection as long as MongoDB accepts it. That means one product document might have productName, another might have name, and a third might accidentally leave out the price. Mongoose lets your application define a more predictable shape before documents are saved.
MongoDB driver versus Mongoose
The MongoDB driver gives direct access to collections and methods such as find, insertOne, updateOne, and deleteOne. Mongoose still uses MongoDB underneath, but it gives your code a higher-level model API.
| Task | MongoDB driver style | Mongoose style |
|---|---|---|
| Get all products | db.collection('products').find({}).toArray() |
Product.find() |
| Create a product | insertOne(productData) |
Product.create(productData) |
| Update a product | updateOne(filter, update) |
Product.findByIdAndUpdate(id, data) |
| Validate required fields | Usually manual validation in app code | Schema validation before save/update |
Example schema
import mongoose from 'mongoose';
const productSchema = new mongoose.Schema({
sku: { type: String, required: true, unique: true },
name: { type: String, required: true },
price: { type: Number, required: true, min: 0 },
category: { type: String, required: true },
isActive: { type: Boolean, default: true }
});
const Product = mongoose.model('Product', productSchema);
export default Product;
This schema says that each product should have a unique SKU, a name, a price, a category, and an active flag. If SKU, name, price, or category is missing, Mongoose can reject the document before it is saved.
Schema field options
Mongoose schemas can include more than just field names and types. They can also include rules and defaults.
required: the field must be present.unique: MongoDB should enforce unique values through an index.default: Mongoose should use this value when the field is omitted.enum: the value must be one of a specific set of allowed values.minandmax: numeric constraints.trim: remove whitespace from the start and end of a string.
const categorySchema = new mongoose.Schema({
slug: { type: String, required: true, unique: true, trim: true },
displayName: { type: String, required: true, trim: true },
department: {
type: String,
enum: ['electronics', 'home', 'clothing', 'outdoor'],
required: true
},
sortOrder: { type: Number, min: 1, required: true },
isVisible: { type: Boolean, default: true }
});
Unique is not the same as required
unique helps MongoDB prevent duplicate values, but it does not automatically mean the field is required. Use required when a value must be present.
A duplicate value for a unique field usually produces a MongoDB duplicate key error, often with code 11000. Your API should map that kind of conflict intentionally, often to 409 Conflict, instead of treating it as an unexpected server error.
Common Mongoose operations
Model.find(): retrieve many documents.Model.findById(id): retrieve one document by MongoDB_id.Model.create(data): validate and insert a new document.Model.findByIdAndUpdate(id, data, options): update a document.Model.findByIdAndDelete(id): delete a document.
Example data-access functions
In a project using MVC-style organization, you might export functions that wrap Mongoose calls. This keeps controller code focused on request and response behavior. In fact, this is a great example of why separating model logic from controller logic is important. You can change the model implementation without affecting the controller code.
The code below is an example of the model code you might find in a file like src/models/product.js. In this example, Product is the Mongoose model created from the schema above. The exported functions are the data-access functions your controllers would call.
import Product from './product-schema.js';
const getAllProducts = async () => {
return Product.find().sort({ name: 1 });
};
const getProductById = async (productId) => {
return Product.findById(productId);
};
const createProduct = async (productData) => {
return Product.create(productData);
};
const updateProduct = async (productId, productData) => {
const allowedUpdates = {};
if (productData.name !== undefined) {
allowedUpdates.name = productData.name;
}
if (productData.price !== undefined) {
allowedUpdates.price = productData.price;
}
if (productData.category !== undefined) {
allowedUpdates.category = productData.category;
}
if (productData.isActive !== undefined) {
allowedUpdates.isActive = productData.isActive;
}
return Product.findByIdAndUpdate(
productId,
allowedUpdates,
{ new: true, runValidators: true }
);
};
const deleteProduct = async (productId) => {
return Product.findByIdAndDelete(productId);
};
export {
getAllProducts,
getProductById,
createProduct,
updateProduct,
deleteProduct
};
The new: true option returns the updated document instead of the original document. The runValidators: true option tells Mongoose to apply schema validation during updates. The example also builds an update object from allowed fields instead of passing the full request body into the database update.
Validation errors in controllers
Mongoose validation is useful, but your controller still needs to return safe, useful API responses. A validation failure should usually be treated as a client-side input problem, often a 400 Bad Request.
const createProductHandler = async (req, res) => {
try {
const product = await createProduct(req.body);
return res.status(201).json(product);
} catch (error) {
if (error.name === 'ValidationError') {
return res.status(400).json({ message: 'Invalid product data' });
}
return res.status(500).json({ message: 'Internal server error' });
}
};
This example does not send raw validation details to the client. In a real project, you might choose to return specific field errors, but you should do so intentionally and consistently.
When a controller uses findById() or findByIdAndUpdate(), an invalid id format can produce a Mongoose CastError. Treat invalid id formats as client input errors, usually 400. Treat a valid id that does not match a document as 404.
Relationships with references
Mongoose can store relationships by saving one document's ObjectId in another document. For example, an ecommerce product could reference a category.
const productSchema = new mongoose.Schema({
sku: { type: String, required: true, unique: true },
name: { type: String, required: true },
price: { type: Number, required: true, min: 0 },
category: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
required: true
},
isActive: { type: Boolean, default: true }
});
Using populate
When a schema uses a reference, the document stores an id. Sometimes the API only needs the id. Other times, the API should return related data. Mongoose's populate method can replace a referenced id with selected fields from the related document.
const product = await Product.findById(productId)
.populate('category', 'displayName slug department');
Use populate carefully. It can make responses more useful, but it also means Mongoose may need to perform additional database work. Your API should only populate related data when the client needs it.
Choosing references or embedded documents
There is no single perfect answer for every relationship. Think about how the data changes and how the API will read it.
- Use a reference when the related resource is managed separately.
- Use a reference when many documents point to the same related document.
- Use embedded data when the related information only matters inside the parent document.
- Use embedded data when you need a historical snapshot that should not change later.
For example, a product might reference the category it belongs to. But an order item might store a snapshot of the product name and price at the time of purchase, because the product's current price could change later.
How Mongoose fits the railway project
In the Week 03 railway project, Mongoose-related issues might ask you to define or improve schemas, add validation, create references between collections, or migrate a route from direct MongoDB calls to Mongoose model methods.
When you work on one of these issues, pay attention to the existing codebase patterns. If the project already has a models folder, controller files, and route files, follow those conventions instead of inventing a new structure.
Activity Instructions
Complete the following:- Find one existing model in the team railway codebase.
- Identify the schema fields, required fields, defaults, enum values, and relationship references.
- Find one controller or route that uses the model.
- Choose a field and decide whether the field should be required, optional, have a default, or have allowed values.
- Find a field that stores a reference to another model, or identify where a reference should exist.
Deliverables Checklist
- You can explain what a Mongoose schema does.
- You can identify a Mongoose model in the team codebase.
- You can explain how Mongoose validation affects API responses.
- You can explain one Mongoose relationship reference.
- You can explain when
populatemight be helpful.
Other Links:
- Return to: Week Overview | Course Home