Skip to main content
Node.js Basics
CHAPTER 22 Beginner

CRUD Operations with MongoDB

Updated: May 13, 2026
35 min read

# CRUD Operations with MongoDB

Welcome to Chapter 22! We have reached a major milestone. You now know how to set up an Express server to handle routes, and you know how to connect Mongoose to a MongoDB database.

It's time to put the two together. In this chapter, we will build a complete API that performs CRUD operations (Create, Read, Update, Delete) directly into our permanent cloud database.

---

1. Introduction

When building a REST API that talks to a database, the operations map perfectly to both HTTP Verbs and Mongoose Methods:

  • Create: HTTP POST -> Mongoose .save() or .create()
  • Read: HTTP GET -> Mongoose .find() or .findById()
  • Update: HTTP PUT -> Mongoose .findByIdAndUpdate()
  • Delete: HTTP DELETE -> Mongoose .findByIdAndDelete()

Mongoose methods are asynchronous. They reach out across the internet to MongoDB Atlas, which takes time. Because of this, we will heavily utilize JavaScript's async / await syntax inside our Express routes.

---

2. Learning Objectives

By the end of this chapter, you will be able to:

  • Use async/await inside Express route handlers.
  • Save new documents to MongoDB using Model.create().
  • Retrieve all documents or a specific document using Model.find().
  • Update existing documents.
  • Delete documents securely.
  • Handle database errors (like invalid IDs) gracefully.

---

3. Beginner-Friendly Explanations

Why async / await?

In older code, you might see Mongoose queries written with .then().
javascript
1
User.find().then(users => res.json(users));

While this works, it gets incredibly messy when you have multiple database calls. Modern Node.js uses async / await. You mark your route handler as async, and then you place await in front of the Mongoose query. Node.js will pause that specific line of code, wait for the database to return the data, and then continue. It makes asynchronous code look perfectly synchronous!

The try / catch Block

Because databases are external, things can go wrong (internet drops, bad data). When using async/await, you must wrap your database logic in a try/catch block. If the database throws an error, the catch block catches it and allows you to send a 500 Server Error to the client instead of crashing your app.

---

4. Syntax Explanation

Let's look at a basic Read (GET) operation using async/await.

```javascript id="ch22-syntax-1" // 1. Mark the callback function as 'async' app.get('/api/users', async (req, res) => { // 2. Open a try/catch block try { // 3. Await the database query // .find() with no arguments returns ALL documents in the collection const allUsers = await User.find(); // 4. Send the result res.status(200).json(allUsers); } catch (err) { // 5. Catch errors! console.error(err); res.status(500).json({ error: "Could not fetch users" }); } });

1234567891011121314151617181920
**Output Explanation:**
When this route is hit, it pauses at `await User.find()`. Once MongoDB returns the array of users, it puts them in the `allUsers` variable, and then Express sends them back as JSON.

---

## 5. Real-world Examples

**The Core of the Web:**
Every single app you use is just a shiny wrapper around CRUD operations.
- **Twitter:** `CREATE` a tweet, `READ` the timeline, `UPDATE` a tweet's like count, `DELETE` your tweet.
- **Amazon:** `CREATE` an order, `READ` product details, `UPDATE` shipping status, `DELETE` an item from the cart.
Once you master CRUD, you can build the backend logic for almost any app on earth.

---

## 6. Multiple Code Examples

Assume we have a Mongoose model named `Student` imported into our `app.js` file.

### Example 1: CREATE (POST)

javascript id="ch22-code-1" app.post('/api/students', async (req, res) => { try { // req.body contains the JSON sent by the client // .create() saves it directly to the DB and returns the new document const newStudent = await Student.create(req.body); res.status(201).json(newStudent); } catch (err) { // If validation fails (e.g., missing required name), it jumps here res.status(400).json({ error: err.message }); } });

1
### Example 2: READ SINGLE (GET by ID)

javascript id="ch22-code-2" app.get('/api/students/:id', async (req, res) => { try { // Grab the ID from the URL const id = req.params.id; // Use Mongoose to find by the _id field const student = await Student.findById(id); if (!student) { return res.status(404).json({ error: "Student not found" }); } res.status(200).json(student); } catch (err) { res.status(500).json({ error: "Invalid ID format or Server Error" }); } });

1
### Example 3: UPDATE (PUT)

javascript id="ch22-code-3" app.put('/api/students/:id', async (req, res) => { try { const id = req.params.id; // Arg 1: ID to find // Arg 2: Data to update it with (req.body) // Arg 3: Options -> new: true returns the UPDATED document instead of the old one const updatedStudent = await Student.findByIdAndUpdate(id, req.body, { new: true }); if (!updatedStudent) { return res.status(404).json({ error: "Student not found" }); } res.status(200).json(updatedStudent); } catch (err) { res.status(500).json({ error: err.message }); } });

1
### Example 4: DELETE (DELETE)

javascript id="ch22-code-4" app.delete('/api/students/:id', async (req, res) => { try { const deletedStudent = await Student.findByIdAndDelete(req.params.id); if (!deletedStudent) { return res.status(404).json({ error: "Student not found" }); } res.status(200).json({ message: "Student successfully deleted" }); } catch (err) { res.status(500).json({ error: err.message }); } });

12345678910111213141516171819202122232425262728293031323334353637383940
---

## 7. Output Explanations

In Example 3, notice the `{ new: true }` option. By default, Mongoose's `findByIdAndUpdate` returns the document *before* the update was applied. When sending data back to a frontend app, they usually want the new data. Adding `{ new: true }` forces Mongoose to return the freshly updated document.

---

## 8. Common Mistakes

1. **Forgetting `await`:** If you write `const users = User.find();`, the `users` variable will just hold a massive, complex "Query Object" instead of the actual data, because you didn't wait for the query to finish!
2. **Missing `app.use(express.json())`:** Your CREATE and UPDATE operations will save empty documents to MongoDB because `req.body` is undefined.
3. **Invalid ObjectIds:** MongoDB IDs are 24 characters. If a user visits `/api/students/123`, Mongoose will throw a "Cast to ObjectId failed" error and your app will crash if it's not inside a `try/catch` block.

---

## 9. Best Practices

- **Sort your queries:** When fetching lists, you usually want the newest items first. You can chain methods: `await Student.find().sort({ createdAt: -1 });`.
- **Select specific fields:** If a user document has a password, you should NOT send it to the client! Use `.select()` to exclude fields: `await User.find().select('-password');`.
- **Consistent Responses:** Keep your API predictable. Always wrap results in an object or use standard HTTP codes.

---

## 10. Exercises

1. Look up the Mongoose documentation for `Model.findOne()`.
2. Write a GET route `/api/students/email/:email`.
3. Extract the email from `req.params`.
4. Use `await Student.findOne({ email: req.params.email })` to find and return that specific student.

---

## 11. Mini Project: Student management API

**Objective:** Build a fully functional, monolithic file containing the connection, the model, and all CRUD routes.

**Step 1:** Installation (`npm install express mongoose`)

**Step 2:** The Code (`server.js`)

javascript id="ch22-mini-project" const express = require('express'); const mongoose = require('mongoose');

const app = express(); app.use(express.json());

// 1. Connection (Replace with your URI!) const dbURI = 'mongodb+srv://admin:admin123@cluster0.abcde.mongodb.net/SchoolDB'; mongoose.connect(dbURI) .then(() => { console.log("DB Connected"); app.listen(3000, () => console.log("Server running on port 3000")); }) .catch(err => console.log(err));

// 2. Schema & Model const studentSchema = new mongoose.Schema({ name: { type: String, required: true }, grade: { type: String, required: true }, enrolled: { type: Boolean, default: true } }); const Student = mongoose.model('Student', studentSchema);

// 3. CRUD ROUTES

// CREATE app.post('/students', async (req, res) => { try { const student = await Student.create(req.body); res.status(201).json(student); } catch (err) { res.status(400).json({ error: err.message }); } });

// READ ALL app.get('/students', async (req, res) => { try { const students = await Student.find(); res.status(200).json(students); } catch (err) { res.status(500).json({ error: err.message }); } });

// UPDATE app.put('/students/:id', async (req, res) => { try { const updated = await Student.findByIdAndUpdate(req.params.id, req.body, { new: true }); res.status(200).json(updated); } catch (err) { res.status(400).json({ error: err.message }); } });

// DELETE app.delete('/students/:id', async (req, res) => { try { await Student.findByIdAndDelete(req.params.id); res.status(200).json({ message: "Student deleted" }); } catch (err) { res.status(400).json({ error: err.message }); } }); ``

*(Test this using Postman by sending POST and GET requests to http://localhost:3000/students!)*

---

12. Coding Challenges

Challenge 1: Create an endpoint DELETE /students. Inside it, use the Mongoose method Student.deleteMany({}) to completely wipe out all students in the database. (Be careful with this powerful command!).

Challenge 2: Modify the READ ALL route to support query parameters. If a user visits /students?grade=A, extract req.query.grade and use await Student.find({ grade: 'A' }) to filter the results dynamically.

---

13. MCQs with Answers

Q1: Which JavaScript keywords are required to handle Mongoose queries cleanly without using .then() callbacks? A) import / export B) try / catch C) async / await D) fetch / resolve Answer: C

Q2: Which Mongoose method retrieves all documents in a collection? A) Model.getAll() B) Model.findAll() C) Model.find() D) Model.read() Answer: C

Q3: Why is a try/catch block necessary when doing database operations? A) It makes the database query run faster. B) It converts the data to JSON automatically. C) It catches network or validation errors and prevents the Node application from crashing. D) It is required to connect to MongoDB Atlas. Answer: C

Q4: In findByIdAndUpdate(), what does the { new: true } option do? A) It creates a new document if one isn't found. B) It returns the freshly updated document instead of the old version. C) It updates the createdAt timestamp. D) It clears the database. Answer: B

---

14. Interview Questions

  1. 1. Why do database operations take time, and how does Node.js handle this?
*Answer:* Database operations are I/O (Input/Output) operations that occur over a network. Since Node.js is single-threaded, it handles this asynchronously. Using
async/await, Node pauses the execution of that specific route handler until the database responds, but the main Event Loop is still free to handle requests from other users simultaneously.
  1. 2. Explain what CRUD stands for and how it maps to HTTP methods.
*Answer:* Create (POST), Read (GET), Update (PUT/PATCH), and Delete (DELETE). These are the four fundamental operations required to manage persistent data.

---

15. FAQs

Q: Should I use findById or findOne? A: Use findById(req.params.id) when searching by the MongoDB _id field (it's slightly faster and handles the ObjectId casting for you). Use findOne({ email: '...' }) when searching by any other specific field.

Q: Do I need a catch block on every single route? A: Currently, yes. However, in advanced Express development, developers write a custom "Error Handling Middleware" that catches all asynchronous errors globally, allowing them to remove try/catch from individual routes to keep code cleaner.

---

16. Summary

  • CRUD forms the foundation of REST APIs.
  • Mongoose methods return Promises, so we must use async/await.
  • Always wrap database queries in try/catch blocks to handle errors gracefully.
  • .create(), .find(), .findByIdAndUpdate(), and .findByIdAndDelete() are your core tools.
  • Set { new: true } to return updated data.

---

17. Next Chapter Recommendation

We can create users, but right now we are saving their passwords in plain text! If our database gets hacked, everyone's accounts are compromised. In Chapter 23: User Authentication in Node.js, we will learn how to securely hash passwords using bcrypt` before saving them to MongoDB!

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·