Basic CRUD with Mongoose
Workplace Context
You are tasked with building the core API for a new blog platform. To start, you need to create the endpoints that allow users to create new posts, view existing posts, update them, and delete them. This set of operations, known as CRUD (Create, Read, Update, Delete), forms the backbone of almost every data-driven application. Your goal is to implement these operations efficiently and reliably using Mongoose models.
Learning Objectives
By the end of this lesson, you will be able to:
- Create new documents in a collection using a Mongoose model.
- Read (query for) documents using methods like
find
andfindById
. - Update existing documents using
findByIdAndUpdate
. - Delete documents from a collection using
findByIdAndDelete
. - Handle potential errors from database operations within an Express application.
The CRUD Operations
CRUD stands for the four fundamental functions of persistent storage: Create, Read, Update, and Delete. In a web application, these typically map directly to HTTP methods in a RESTful API:
- Create ->
POST
- Read ->
GET
- Update ->
PUT
/PATCH
- Delete ->
DELETE
Mongoose models provide a rich, easy-to-use API for performing all of these operations.
For these examples, let’s assume we have a Post
model defined from a schema.
const postSchema = new Schema({
title: { type: String, required: true },
author: { type: String, required: true },
body: String,
comments: [{ body: String, date: Date }],
hidden: Boolean,
});
const Post = mongoose.model('Post', postSchema);
Create (Creating Documents)
To add a new document to a collection, you can create a new instance of your model and call the .save()
method on it, or you can use the static Model.create()
method. Both are asynchronous and return a promise.
new Model().save()
This method is useful if you need to modify the document object in your code before saving it.
const newPost = new Post({
title: 'My First Post',
author: 'John Doe',
body: 'This is the beginning of a beautiful blog.',
});
// The save method returns a promise that resolves with the saved document
newPost.save()
.then(savedPost => {
console.log('Post saved successfully:', savedPost);
})
.catch(err => {
console.error('Error saving post:', err);
});
Model.create()
This is a convenient shorthand for creating one or more documents. You pass the data object directly, and Mongoose handles the new
and .save()
calls for you.
// This method is often cleaner and more direct
Post.create({
title: 'A Simpler Way',
author: 'Jane Doe',
body: 'The create method is very handy.',
})
.then(createdPost => {
console.log('Post created successfully:', createdPost);
})
.catch(err => {
console.error('Error creating post:', err);
});
Read (Querying for Documents)
Mongoose provides a powerful set of methods for finding documents.
Model.find(query)
This is the most common query method. It retrieves all documents that match the query
object.
// Find all posts by a specific author
Post.find({ author: 'John Doe' })
.then(posts => {
console.log('Found posts:', posts); // `posts` will be an array of documents
});
// If you pass an empty object, it finds ALL documents in the collection
Post.find({})
.then(allPosts => {
console.log('All posts in the collection:', allPosts);
});
Querying on Multiple Conditions
You can provide multiple key-value pairs in your query object to find documents that match all the specified criteria.
// Find all posts by 'John Doe' that are NOT hidden
Post.find({ author: 'John Doe', hidden: false })
.then(posts => {
console.log('Found visible posts by John Doe:', posts);
});
Complex Queries with Operators
While providing multiple key-value pairs creates an implicit “AND” condition, MongoDB provides a rich set of query operators for more complex scenarios. These operators are prefixed with a dollar sign ($
).
The $or
operator performs a logical OR operation on an array of two or more expressions and selects the documents that satisfy at least one of the expressions.
// Find posts that are either hidden OR written by 'Jane Doe'
Post.find({
$or: [
{ hidden: true },
{ author: 'Jane Doe' }
]
})
.then(posts => {
console.log('Found posts that are hidden or from Jane Doe:', posts);
});
Similarly, the $and
operator can be used to explicitly join query conditions. This is useful when you need to combine conditions on the same field.
// Find products with a price greater than 10 but less than 50
// Assumes a Product model with a 'price' field
Product.find({
$and: [
{ price: { $gt: 10 } }, // $gt means "greater than"
{ price: { $lt: 50 } } // $lt means "less than"
]
})
Model.findOne(query)
This method is like find
, but it only returns the first document that matches the query, or null
if no match is found. It’s highly efficient for finding a unique item.
Model.findById(id)
This is a shorthand for findOne({ _id: id })
. Since querying by ID is extremely common, Mongoose provides this dedicated helper.
// Assume we have an ID from a URL parameter
const postId = '60c72b2f9b1e8a5f4d6e8b21';
Post.findById(postId)
.then(post => {
if (post) {
console.log('Found post:', post);
} else {
console.log('No post found with that ID.');
}
});
Update (Updating Documents)
To update a document, you typically want to find it and apply changes in a single atomic operation.
A what? An “atomic operation” is a single, indivisible operation that is performed in a single step. It is either completed or not, and it cannot be interrupted or partially completed. This helps prevent race conditions and ensures data consistency.
Atomic operations are a fundamental concept in database design and are supported by MongoDB.
Model.findByIdAndUpdate(id, update, options)
This is the most common and recommended method for updating. It finds a document by its ID, applies the update
, and returns the document.
id
: The_id
of the document to update.update
: An object containing the fields to change.options
: An optional configuration object. The most important option isnew: true
, which tells Mongoose to return the modified document instead of the original one.
const postIdToUpdate = 'some_id_here';
const updateData = {
title: 'My Updated Post Title',
hidden: true
};
const options = { new: true }; // Return the document *after* the update
Post.findByIdAndUpdate(postIdToUpdate, updateData, options)
.then(updatedPost => {
console.log('Post updated:', updatedPost);
});
Since this is an atomic operation, it will either update the document or return an error if it fails. Any reads that happen on the document will return the updated values.
Upserting: Update or Insert
What if you want to update a document if it exists, but create it if it doesn’t? This common pattern is called an upsert (a combination of “update” and “insert”).
You can achieve this by setting the upsert: true
option in your update method. This is more commonly used with findOneAndUpdate
than findByIdAndUpdate
, since you are often looking for a document based on a specific field other than its _id
.
const query = { title: 'A Brand New Post' }; // The criteria to find the document
const updateData = {
author: 'System',
body: 'This post was auto-generated.'
};
// new: returns the modified doc
// upsert: creates the doc if it doesn't exist
const options = { new: true, upsert: true };
Post.findOneAndUpdate(query, updateData, options)
.then(result => {
console.log('Upserted document:', result);
});
In this example, if a post with the title “A Brand New Post” exists, it will be updated. If it does not exist, a new document will be created with both the query
and updateData
fields.
Delete (Removing Documents)
Deleting documents is straightforward.
Model.findByIdAndDelete(id)
This method finds a document by its ID and deletes it from the database. It returns the deleted document, which can be useful for confirmation.
const postIdToDelete = 'some_id_here';
Post.findByIdAndDelete(postIdToDelete)
.then(deletedPost => {
if (deletedPost) {
console.log('Successfully deleted post:', deletedPost.title);
} else {
console.log('Could not find post to delete.');
}
});
Handling Errors
When you perform any of these operations in a real application (like an Express API), you need to handle potential errors. The most common way to do this is with async/await
and a try...catch
block.
// Example in an Express route handler
app.post('/posts', async (req, res) => {
try {
// req.body contains the data from the client
const newPost = await Post.create(req.body);
res.status(201).json(newPost); // Send back the created post
} catch (error) {
// If validation fails or another DB error occurs
console.error('Error creating post:', error);
res.status(400).json({ error: 'Failed to create post', details: error.message });
}
});
Activities
Activity 1: Build a POST /products
Endpoint
- Create a simple Express server connected to your MongoDB database.
- Define a
Product
model with fields likename
(required String),price
(required Number), andcategory
(String). - Create an Express route handler for
POST /products
. - Inside the handler, use
async/await
and atry...catch
block. - In the
try
block, useProduct.create(req.body)
to create a new product from the incoming request body. - Send a
201 Created
status and the new product as a JSON response. - In the
catch
block, send a400 Bad Request
status and an error message. - Test this endpoint using an API client like Postman.
Activity 2: Build a GET /products
Endpoint
- Create a route handler for
GET /products
. - Use
Product.find({})
to retrieve all products from the database. - Send the array of products back as a JSON response.
Activity 3: Build a DELETE /products/:id
Endpoint
- Create a route handler for
DELETE /products/:id
. - Get the product ID from
req.params.id
. - Use
Product.findByIdAndDelete(id)
to remove the product. - If a product was deleted, send back a success message. If no product was found with that ID, send a
404 Not Found
status.
Knowledge Check
Which Mongoose method is best suited for creating a new document in the database from a simple data object?
- Select an answer to view feedback.
What does Model.find({ name: 'test' })
return if no documents match the query?
- Select an answer to view feedback.
Summary
In this lesson, you learned how to perform the four essential CRUD operations using a Mongoose model. You can now Create documents with create
, Read them with find
and findById
, Update them with findByIdAndUpdate
, and Delete them with findByIdAndDelete
. You also saw how to structure these database calls within an Express application using async/await
and try...catch
blocks to build robust API endpoints. These skills are the foundation for building any data-driven backend service.
References
- Mongoose Docs: Queries
- Mongoose Docs:
Model.create()
- Mongoose Docs:
Model.find()
- Mongoose Docs:
Model.findById()
- Mongoose Docs:
Model.findByIdAndUpdate()
- Mongoose Docs:
Model.findByIdAndDelete()
- MongoDB Docs: Query and Projection Operators