DEV Community

Cover image for Setting up MongoDB and Mongoose for your project
mech2dev
mech2dev

Posted on

Setting up MongoDB and Mongoose for your project

Overview

In this tutorial, we will look at how to set up our project from my previous tutorial to use MongoDB.

In the previous article, we built a REST API that served data to our blog application. We build out endpoints to create, read, update and delete data. This data however is stored in an array in our blogController.js file.

This works when testing the application but all the changes made are temporary. If you create a new blog entry, this blog entry will remain visible but disappears when you restart the server.

We will now introduce a database to our project so as to persist our data. There are many different choices for a database depending on your applications needs.

For our application, we will use MongoDB with Mongoose.

MongoDB is a NoSQL document database used to build highly available and scalable applications.

Mongoose is an ODM (Object Data Modelling) library used to make it easier to connect to MongoDB by providing functions to manipulate the documents in collections.

REST API for a React FrontEnd

This is an express rest api for a simple blog app that enables one to manage a simple blog.

You can view the client app here .

Endpoints

URL METHOD DESCRIPTION
api/blogs/ GET fetches all blogs
api/blogs/{id} GET fetches a single blog
api/blogs/ POST post a blog
api/blogs/{id} DELETE delete a single blog



Project Structure

We have already built out the skeleton of this project in my previous article. As a recap, here's what we have so far.

Project structure

We created a directory called server. In this server directory we have our server.js file which is our entry point to our API. We also have routes and controllers directories which hold the routes and the controllers (callback functions that are fired when these routes are matched) respectively.

That's the structure we have so far. Next we will do the database configuration.

Connecting to MongoDB

As described before, MongoDB is a document based database that allows us to build highly scalable applications. You can install MongoDB server locally but for our case, we will use MongoDB Atlas.

MongoDB Atlas is a fully-managed cloud database that handles all the complexity of deploying, managing, and healing your deployments on the cloud service provider of your choice (AWS , Azure, and GCP).

To get started, lets navigate to MongoDB homepage, create an account and login.

Click on "Create new organization" and give your organization a name. Let's call our organization tutorials and choose MongoDB Atlas as our preferred Cloud Service. Click on next and confirm organization creation on the next page.

create organization

Once the organization is created, we need to initialize a new project. Once you click on "create organization" you will be redirected to a projects page which is empty so far. Let's create a new project. Click on new project as below and name it dojo-blog-app. This is in line with the name of the blog app we are creating this database for. Confirm project creation on the subsequent pages.

creating new project

With our project up and running, we now need to create a database. Indeed after confirming project creation, we are now redirected to a "Create database" page. Let's click on "Build Database" and create a database for our project.

create database

We are now redirected to choose a hosting plan. We, for the purposes of this tutorial, will use the free shared plan.

create clusters

We will now be required to do some configuration for our shared cluster. As a tip, most of the recommended settings here can do. The only change we will make is naming our cluster something memorable than cluster0. Lets call our cluster dojo-cluster. Click on "Create".

Next up is a few more settings to configure the security of our database and clusters. You'll be asked how you would like to authenticate your connection (username and password) and where you would like to connect from (local environment).

Create a username and password.

username and password

Select "My local environment" as where you want to connect from and also add your current IP address to the list of whitelisted IP addresses.

where to connect from

Click on finish and close and we are now done setting up our database. Under our created project, we can now see the cluster we have created (dojo-cluster).

cluster confirmation

With the clusters up and running, let's now focus on connecting our database to the rest of our REST API (no pun intended). Navigate to your cluster (like in the above screenshot) and click on "Connect".

There are several options to connect. Primarily, we want to get a connection string that will be domiciled in our dotenv file that will enable us to connect to our database.

Conection options

Let's click on the second option which is "Connect your application to your cluster using MongoDB's native drivers". Here we can now get a string that we will copy and paste in our dotenv file in our application code.

connection string

Copy the connection string and save it somewhere as we will need it up next as we refactor our existing code to use our database.

Connecting MongoDB to our project

In my previous tutorial/article, we saw how we can set up a REST API for a simple blog app to give it some CRUD functionality without having a functional database.

We are now implementing the database in this phase of the project.

In the previous section, we created a MongoDB Atlas account and set up a cluster. In this section, we will now connect our app to the cluster.

Create a new directory inside the server directory and lets name it config. Inside config lets create a file called db.js and this is where we will write all our database connection code.

Copy and paste the code below into your db.js file.

const mongoose = require('mongoose')

const connectDB = async() => {
    try {
        const conn = await mongoose.connect(process.env.MONGO_URI)
        console.log(`Database connected ${conn.connection.host}`.cyan.underline)
    } catch (error) {
        console.log(error)
        process.exit(1)
    }
}

module.exports = {
    connectDB
}
Enter fullscreen mode Exit fullscreen mode

We begin by importing mongoose into our project (do npm install mongoose if not installed yet). We will then create a function called connectDB. This is the function we will call when we want to connect to our database in our main server.js file. This is an asynchronous function since we are now working with a database.

In connectDB, we will use the Mongoose method mongoose.connect to connect to our cluster. This method takes in a connection string as an argument.

Remember the connection string we copied and stored somewhere safe? We will now create an environment variable MONGO_URI and give it the value of the stored string as seen below.

NB: Remember to replace password with the actual password to the cluster that you created.

dotenv

We then log a statement to our console to show that our connection is successful. The cyan.underline appended to the console.log() statement changes the color of the logged statement to cyan and is a feature of the colors package (do npm install colors if not installed yet).

The catch part of our try catch statement simply logs an error if any and exits the process. Lastly, we do a module.exports statement so as to export the function connectDB to be used elsewhere, in this case the server.js file.

Creating models

Now that we have the connection to the database up and running, let us create models for our database. We will first define the structure of our database using a schema.

Generally speaking, a schema is a structured framework or an outline. A database schema therefore defines how data is organized within a database including logical constraints such as, collection names, fields, data types, and the relationships between these entities.

Create a new directory named models where we will store our various data models. For our blog app, create a new file in the models directory called blogModel.js.

At the moment we are only concerned with a simple blog app but later we may need to add more functionality such as users etc and thus we may need to create new models. These models will be created in separate files eg userModel.js. Keeping each model separate is a good practice that ensures modularization of our code.

Let's have a look at blogModel.js.

const mongoose = require('mongoose')

const blogSchema = mongoose.Schema({
    title:{
        type:String,
        required:[true, 'Please add a title']
    },
    body:{
        type:String,
        required:[true, 'Please add a story']
    },
    author:{
        type:String,
        required:[true, 'Please add an author name']
    }
},{
    timestamps:true,
})

module.exports = mongoose.model('Blog', blogSchema)
Enter fullscreen mode Exit fullscreen mode

As usual, we start by importing Mongoose. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection.

To create our database schema, we will use the very handy Mongoose.schema() method to define blogSchema. Our blog entries have 3 fields i.e. title, author, and the blog body. MongoDB adds a unique ID field for us so we wont add that in our schema definition.

Read more about mongoose and schemas here.

After defining our fields and the data types in the schema, we can add another argument to our Mongoose.schema() method that adds timestamps for when we create or update posts.

As always, don't forget to export the model so that we can use it in our controller functions. We will use the mongoose.model() method. The mongoose.model() function of the mongoose module is used to create a collection of a particular database of MongoDB.

Creating controllers

Now that we have a single function we can fire up to connect to the database, let us re-purpose our controller functions to read, write or delete data from our database and not the array we were using before.

Let's see how our controllers/blogControllers.js file will now look like and then we will discuss the code below.

const asyncHandler = require('express-async-handler')
const Blog = require('../models/blogModel')


// @desc Get all blogs
// @route /api/blogs/
// @access public
const getBlogs = asyncHandler(async(req, res) => {
    const blogs = await Blog.find()
    res.json(blogs)
})


// @desc Get single blog
// @route /api/blogs/{id}
// @access public
const getBlog = asyncHandler(async(req, res) => {

    const blog = await Blog.findById(req.params.id)

    if(!blog){
        res.status(400)
        throw new Error('Blog not found')
    }

    res.json(blog)

})


// @desc Post a blog
// @route /api/blogs/{id}
// @access public
const postBlog = asyncHandler(async(req, res) => {

    if (!req.body.title || !req.body.body || !req.body.author){
        res.status(400)
        throw new Error ('Please fill all the required fields')

    } else{
        const blog = await Blog.create({
            title: req.body.title,
            body: req.body.body,
            author: req.body.author
        })

        res.status(200).json(blog)
    }
})


// @desc Delete a blog
// @route /api/blogs/{id}
// @access public
const deleteBlog = asyncHandler(async(req, res) => {

    const blog = await Blog.findById(req.params.id)
    if(!blog){
        res.status(400)
        throw new Error('Blog not found')
    }

    await blog.remove()

    res.status(200).json({message:"blog deleted"})
})


module.exports = {
    getBlogs,
    getBlog,
    postBlog,
    deleteBlog
}
Enter fullscreen mode Exit fullscreen mode

We have 4 controller functions which are fired up when specific routes are matched. Our API has the following endpoints:

URL METHOD DESCRIPTION
api/blogs/ GET fetches all blogs
api/blogs/{id} GET fetches a single blog
api/blogs/ POST posts a new blog
api/blogs/{id} DELETE deletes a single blog

In blogController.js, we start by importing asyncHandler. asyncHandler wraps async functions and saves you writing your own try/catch for async/await and passes errors on to next.

We will also import the Blog model we created in our models directory

To get all blogs, we have:

const getBlogs = asyncHandler(async(req, res) => {
    const blogs = await Blog.find()
    res.json(blogs)
})
Enter fullscreen mode Exit fullscreen mode

Using the Blog model, we will use the find() method provided by mongoose. find() returns all records found. Sometimes you may need to return a particular record by id etc and there are methods to do that. Read more about mongoose queries here.

To get a specific blog, we have the following code:

const getBlog = asyncHandler(async(req, res) => {

    const blog = await Blog.findById(req.params.id)

    if(!blog){
        res.status(400)
        throw new Error('Blog not found')
    }

    res.json(blog)
})
Enter fullscreen mode Exit fullscreen mode

To return a specific blog post, we make use of the mongoose query findById(). We then check whether the blog with the requested id exists. We get the id from the request using req.params.id. If not we show an error message of 'Blog not found'. Otherwise we return the requested post.

To post a blog we have the following code:

const postBlog = asyncHandler(async(req, res) => {

    if (!req.body.title || !req.body.body || !req.body.author){
        res.status(400)
        throw new Error ('Please fill all the required fields')

    } else{
        const blog = await Blog.create({
            title: req.body.title,
            body: req.body.body,
            author: req.body.author
        })

        res.status(200).json(blog)
    }
})
Enter fullscreen mode Exit fullscreen mode

We will use an if/else block to take care of empty entries. After checking that all fields have been filled, we then use the Model.create() method to create a new document in our collection. If successful, we return the created blog with a status code of 200.

To delete a blog, we have:

const deleteBlog = asyncHandler(async(req, res) => {

    const blog = await Blog.findById(req.params.id)
    if(!blog){
        res.status(400)
        throw new Error('Blog not found')
    }

    await blog.remove()

    res.status(200).json({message:"blog deleted"})
})
Enter fullscreen mode Exit fullscreen mode

We target the post we want to delete using findById(). Once located, we use mongoose's remove() method to remove it.

Custom error handling middleware

One advantage of using Express.js is that it is unopinionated and allows us the flexibility to tackle challenges using custom solutions.

This is in contrast to opinionated frameworks (e.g. Django) which have set rules to tackle most problems.

To illustrate the unopinionated nature of express, we will build a custom error handler and chain it to all our requests as middleware.

In our root directory of server, lets create a middleware directory and inside it create the file errorHandler.js. In this file, we will implement some custom error handling. This will include the option to have verbose or abbreviated error messages depending on whether the app is in development or production mode.

Let's have a look at errorHandler.js and discuss it below:

const errorHandler = (err, req, res, next) => {

    const statusCode = (res.statusCode ? res.statusCode : 500)

    res.status(statusCode)

    res.json({
        message: err.message,
        stack: process.env.NODE_ENV === 'development' ? err.stack : 'null'
    })
}

module.exports = {
    errorHandler
}
Enter fullscreen mode Exit fullscreen mode

We will define an error handler function which takes in 4 arguments (i.e. err, res, req and next).

In the callback, we check whether the response has a statusCode. If not we return a generic 500 (Internal server error). We then want to return an error message (from err.message) and a full description of the error (from err.stack) when process.env.NODE_ENV === 'development'. If we are in production, we want to see a more abbreviated description of the error (in this case null).

Testing the API

Congratulations. We have built a fully functional REST API complete with a supporting MongoDB database. In my previous article, we tested our API using Postman and all the endpoints worked as they should.

We shall follow a similar approach and retest our endpoints seeing as we have made significant changes to our API.

Lets fire up Postman and start by testing GET /api/blogs/:

test get all blogs

As expected, we retrieve all blogs we have at the moment i.e. 2 blog posts.

Next we will use one of the blog id's we get from requesting all blogs above to test our endpoint that retrieves a single blog i.e. GET /api/blogs/{id}:

get single post

Voila. Both retrieving all blogs and retrieving a single blog work as expected.

I will leave the two remaining methods i.e. POST and DELETE for you the reader to test them out tell me what you get by leaving a comment.

Conclusion

That's the end of this particular article. We have come a long way from building dummy express apps to now building a fully functional rest api.

To build further on this, we will add some type of authentication to our app in the next article.

Till then, adios.

Setting up MongoDB

Connecting MongoDB to our project

Changing code to fit MongoDB (creating schemas etc)

Testing with Postman

Top comments (2)

Collapse
 
naucode profile image
Al - Naucode

Hey, it was a nice read, you got my follow, keep writing!

Collapse
 
danielstai profile image
mech2dev

Thank you so much for the like and follow and for the kind words too.