Hello there! Today, we're diving into the world of databases. We'll learn how to use MongoDB, a popular NoSQL database, with Mongoose, an elegant Object Data Modeling (ODM) solution for MongoDB and Node.js. By the end of this lesson, you will understand why MongoDB and Mongoose are essential tools in backend development and how to harness their power to store and manage data for your application.
Let's start our journey with a brief introduction to MongoDB. It is a database program that is designed to handle large amounts of data across many servers. It falls under the NoSQL category, meaning it does not rely on the traditional table-based format that SQL databases use. Instead, MongoDB holds data in a flexible, JSON-like format known as BSON (Binary JSON).
In MongoDB, data is organized into documents and collections, similar to how we would organize data in objects and arrays in JavaScript. These collections are then stored in databases. This structure can handle all types of data, making MongoDB a versatile choice for many different types of applications.
For instance, a MongoDB document for a to-do list item might look like this:
JSON1{ 2 "_id" : ObjectId("543d9f85c1ffa014dcfa648c"), 3 "task" : "Do the dishes", 4 "status": false 5}
This is the equivalent of a row in an SQL database.
Mongoose is an ODM that provides a solution to work in an asynchronous environment. It serves as a layer of abstraction, allowing us to interact with MongoDB through our application using JavaScript.
Using Mongoose, we can define objects with strongly typed schemas that map directly to a MongoDB document. These objects, called models, contain all the functions and hooks we need to query and modify the data in our database.
By defining a schema and model for our to-do list items, we can create new items, update or delete existing ones, retrieve items with specific properties, and more, all using simple JavaScript.
Let's take a look at an example of a Mongoose schema and model:
JavaScript1const mongoose = require('mongoose'); 2 3const TodoSchema = new mongoose.Schema({ 4 task: { 5 type: String, 6 required: true 7 }, 8 status: { 9 type: Boolean, 10 default: true 11 } 12}); 13 14const Todo = mongoose.model('Todo', TodoSchema); 15 16module.exports = Todo;
In Mongoose, you create a schema using mongoose.Schema
, which defines the structure of your documents. For example, you can define fields such as task
, which holds the task name, and status
, which indicates its current state.
Each field in the schema can have:
type
: Specifies the expected data type, ensuring data consistency.required
: Enforces that a value must be provided before saving.default
: Assigns a default value if none is provided during creation.
Once the schema is defined, you create a model using mongoose.model
. This model represents a collection and provides built-in methods for creating, reading, updating, and deleting documents.
Now that we have our database set up, how do we use this in our Node.js and Express application? Mongoose makes this process straightforward with the connect()
function.
To connect, we simply pass our MongoDB connection URI (the string we obtained in MongoDB Atlas) to mongoose.connect()
:
JavaScript1const mongoose = require('mongoose'); 2 3mongoose.connect('mongodb+srv://username:password@cluster.mongodb.net/database', { 4 useNewUrlParser: true, 5 useUnifiedTopology: true 6}) 7.then(() => console.log('Connected to MongoDB')) 8.catch((error) => console.log(`Could not connect to MongoDB: ${error}`));
The useNewUrlParser: true
option ensures that the MongoDB driver uses the new connection string parser to properly handle connection strings that include both the hostname and options in URI formats. The useUnifiedTopology: true
option enables the new unified topology layer, which improves server discovery and monitoring, and reduces connection behavior inconsistencies by using the latest MongoDB driver logic.
With these few lines of code, our Node.js and Express application is now connected to a MongoDB database!
Before diving into our CRUD (Create, Read, Update, Delete) operations, let's briefly discuss the async
and await
keywords, which are essential for handling asynchronous operations in JavaScript. These help us write asynchronous code that looks synchronous, making it easier to read and maintain.
async
is used to declare an asynchronous function. It allows the use ofawait
within the function.await
pauses the execution of the function until the promise is resolved or rejected. It can only be used inside anasync
function.
When working with databases like MongoDB through Mongoose, operations are typically asynchronous because they involve I/O operations that take time. Using async
/await
ensures that our code waits for these operations to complete before proceeding, which helps in handling the results correctly and improves the readability and maintainability of our code.
It's important to note that async
/await
does not make JavaScript multithreaded. JavaScript remains single-threaded and uses an event loop to handle asynchronous operations without blocking the main thread. These keywords allow better management of asynchronous code, making our functions appear synchronous, but they do not add parallel execution capabilities.
Here’s a simple example:
JavaScript1async function fetchData() { 2 try { 3 const data = await someAsyncOperation(); 4 console.log(data); 5 } catch (error) { 6 console.error(error); 7 } 8}
In this example, someAsyncOperation()
is a placeholder for any operation that returns a promise. The await
keyword pauses the function execution until the promise is resolved, allowing data
to be assigned the resolved value, or throwing an error if the promise is rejected.
Now, let's apply this understanding to our CRUD operations with Mongoose.
To retrieve all todo items from the database, we can use the find()
method provided by Mongoose. Here’s how you implement the GET route:
JavaScript1app.get('/api/todos', async (req, res) => { 2 try { 3 // Use the Model's find() method 4 const todos = await Todo.find(); 5 res.json(todos); 6 } catch (error) { 7 res.status(500).send(error); 8 } 9});
This route will fetch all todos from the database and return them in JSON format. If there's an error, it will send a 500 status code with the error message.
To add a new todo item to our database, we can create a new instance of the Todo
model and use the save()
method to store it. Here's the POST route:
JavaScript1app.post('/api/todos', async (req, res) => { 2 try { 3 const newTodo = new Todo({ 4 task: req.body.task, 5 status: req.body.status 6 }); 7 const savedTodo = await newTodo.save(); 8 res.status(201).json(savedTodo); 9 } catch (error) { 10 res.status(400).send(error); 11 } 12});
In this route, we create a new todo object from the request body and save it to the database. It returns the newly created todo and a 201 status code indicating the resource was successfully created. If there's an error with saving, it will return a 400 status code.
To update a todo item, we can use the findByIdAndUpdate()
method provided by Mongoose. Here’s the PUT route:
JavaScript1app.put('/api/todos/:id', async (req, res) => { 2 try { 3 const updatedTodo = await Todo.findByIdAndUpdate(req.params.id, req.body, { new: true }); 4 res.json(updatedTodo); 5 } catch (error) { 6 res.status(400).send(error); 7 } 8});
This route takes the id
from the request parameters and the updated data from the request body. It locates the corresponding todo in the database and updates it with the new data. The { new: true }
option ensures that the response will contain the updated document. If there's an error with the update, it will return a 400 status code.
To delete a todo item, we can use the findByIdAndDelete()
method provided by Mongoose. Here’s the DELETE route:
JavaScript1app.delete('/api/todos/:id', async (req, res) => { 2 try { 3 await Todo.findByIdAndDelete(req.params.id); 4 res.status(204).send(); 5 } catch (error) { 6 res.status(500).send(error); 7 } 8});
This route takes the id
from the request parameters, locates the corresponding todo in the database, and deletes it. On successful deletion, it sends a 204 status code to indicate that the operation was successful with no content to return. If there's an error, it sends a 500 status code.
These implementations show how easily we can perform CRUD operations using Mongoose within our Express application, allowing us to manage our todo items efficiently.
Fantastic! You've tackled MongoDB, Mongoose, and their incorporation in a Node.js and Express application. You can set up a MongoDB database, connect your application to the database using Mongoose, and interact with your database. Now, jump straight into the hands-on exercises to refine and apply your newfound skills. Happy learning!