CRUD Operations with MongoDB
# 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/awaitinside 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().
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" }); } });
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 }); } });
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" }); } });
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 }); } });
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 }); } });
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. Why do database operations take time, and how does Node.js handle this?
, 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.
-
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!