Skip to Content
Lesson 4

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 and findById.
  • 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.

models/Post.js
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 is new: 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

  1. Create a simple Express server connected to your MongoDB database.
  2. Define a Product model with fields like name (required String), price (required Number), and category (String).
  3. Create an Express route handler for POST /products.
  4. Inside the handler, use async/await and a try...catch block.
  5. In the try block, use Product.create(req.body) to create a new product from the incoming request body.
  6. Send a 201 Created status and the new product as a JSON response.
  7. In the catch block, send a 400 Bad Request status and an error message.
  8. Test this endpoint using an API client like Postman.

Activity 2: Build a GET /products Endpoint

  1. Create a route handler for GET /products.
  2. Use Product.find({}) to retrieve all products from the database.
  3. Send the array of products back as a JSON response.

Activity 3: Build a DELETE /products/:id Endpoint

  1. Create a route handler for DELETE /products/:id.
  2. Get the product ID from req.params.id.
  3. Use Product.findByIdAndDelete(id) to remove the product.
  4. 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


Additional Resources