Build a Node.js REST API with Express and MongoDB
Tags: node, mongodb, express, api
Published: 2021-05-11 10:09:58

In this tutorial, we will create a Movie Catchphrase API that allows you to Create, Read, Update and Delete Catchphrases, or in short perform CRUD operations.

We are going to use Node.js and Express with Mongoose to interact with the MongoDB instance. We will use Swagger to document the API we created.

## **MongoDB Setup**

For this project, I assume you already have set up a MongoDB cluster (or a local MongoDB installation) and have the connection URI. If not you can refer to these links for an installation guide: [MongoDB cluster](https://docs.atlas.mongodb.com/getting-started/) or [MongoDB local](https://docs.mongodb.com/manual/administration/install-community/)

## **Project Setup**

The first thing we need to do is set up the project by initializing with npm and installing the packages we are going to use. Run the following commands to set up the project:

```bash
npm init -y
npm install --save express mongoose
npm install --save-dev dotenv nodemon
```

`dotenv` will allow us to pull in environment variables from a `.env` file. Create a `.env` file in the root of the project and add the following:

```plaintext
MONGO_URI=Your_MongoDB_URI_comes_here
```

Next, let's create a `.gitignore` file in the root of the project and add the following:

```plaintext
.env
node_modules
```

Change the `package.json` scripts with the following:

```json
"scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
},
```

> Note: Nodemon allows you to keep the application running while making changes.

## **Start Building the API**

Let's create a `server.js` file at the root of the project. This will contain a basic server setup with a basic route. Add the following to the file:

```javascript
const express = require('express');

const app = express();

app.use(express.json());

app.get('/', (req, res) => {
  res.send('Hello World!')
});

app.listen(process.env.PORT || 5000, () => console.log('Up and running 🚀'));
```

In order to start the application, run the following command:

```bash
npm run dev
```

Navigate to `localhost:5000` in the browser to view the application.

## **Configuring and Connecting to the database**

Always keep all the configurations for the app in a separate folder. Let’s create a new folder `config` in the root folder of our application for keeping all the configurations.

Create a new file `db.js` inside the config folder with the following contents:

```javascript
const mongoose = require('mongoose');
require("dotenv").config();

const connectDB = async () => {
    try {
        const conn = await mongoose.connect(process.env.MONGO_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
            useFindAndModify: false,
        });

        console.log(`MongoDB Connected: ${conn.connection.host}`);
    } catch (err) {
        console.error(err);
        process.exit(1);
    }
};

module.exports = connectDB;
```

We are going to import the above database configuration in `server.js` and call the `connectDB` function to connect to our MongoDB database. Update the `server.js` accordingly:

```javascript
const express = require('express');
const connectDb = require("./config/db");

const app = express();
connectDb();

app.use(express.json());

app.get('/', (req, res) => {
  res.send('Hello World!')
});

app.listen(process.env.PORT || 5000, () => console.log('Up and running 🚀'));
```

## **Creating the Catchphrase Model**

Let’s create a new folder `models` in the root folder of our application for keeping all the models.

Create a new file `catchphrase.js` inside the models folder with the following contents:

```javascript
const mongoose = require("mongoose");
const  Schema  = mongoose.Schema;

const catchphraseSchema = new Schema({
    movieName: {
        type: String,
    },
    catchphrase: {
        type: String,
    },
    movieContext: {
        type: String,
    },
});

const Catchphrase = mongoose.model('Catchphrase', catchphraseSchema);

module.exports = Catchphrase;
```

## **Creating the Catchphrases Controller**

Let’s create a new folder `controllers` in the root folder of our application for keeping all the controllers.

Create a new file `catchphraseController.js` inside the controllers folder with the following contents:

```javascript
const Catchphrase = require('../models/catchphrase');

async function getAllCatchphrases(search, reqPage, reqLimit) {
    let options = {};

    if (search) {
        options = {
            ...options,
            $or: [
                {movieName: new RegExp(search.toString(), 'i')},
                {catchphrase: new RegExp(search.toString(), 'i')}
            ]
        }
    }

    let total = Catchphrase.countDocuments(options);
    let page = parseInt(reqPage) || 1;
    let limit = parseInt(reqLimit) || parseInt(await total);
    let last_page = Math.ceil(parseInt(await total)/limit);
    if (last_page < 1 && total > 0) {
        last_page = 1
    }

    try {
        const catchphrases = await Catchphrase.find(options).skip((page - 1) * limit).limit(limit);
        return {
            success: true,
            data: catchphrases,
            total: (await total).toString(),
            page: (await page).toString(),
            last_page: (await last_page).toString(),
        };
    } catch (err) {
        return { success: false, message: "Catchphrases not found" };
    }
}

async function getCatchphraseById(id) {
    let catchphrase;
    try {
        catchphrase = await Catchphrase.findById(id);
        if (catchphrase == null) {
            return { success: false, message: 'Cannot find catchphrase' };
        }
    } catch (err) {
        return { success: false, message: err.message };
    }

    return {
        success: true,
        data: catchphrase,
    };
}

async function addCatchphrase(body) {
    const catchphrase = new Catchphrase(body);

    try {
        const newCatchphrase = await catchphrase.save();
        return {
            success: true,
            data: newCatchphrase,
        };
    } catch (err) {
        return { success: false, message: "Failed to add catachphrase" };
    }
}

async function updateCatchphrase(id, movieName = null, reqCatchphrase = null, movieContext = null) {
    let catchphrase;
    try {
        catchphrase = await Catchphrase.findById(id);
        if (catchphrase == null) {
            return { success: false, message: 'Cannot find catchphrase' };
        }
        if (movieName != null) {
            catchphrase.movieName = movieName
        }
        if (reqCatchphrase != null) {
            catchphrase.catchphrase = reqCatchphrase
        }
        if (movieContext != null) {
            catchphrase.movieContext = movieContext
        }

        try {
            const updatedCatchphrase = await catchphrase.save()
            return {
                success: true,
                data: updatedCatchphrase,
                message: "Catchphrase updated successfully"
            };
        } catch (err) {
            return { sucess: false ,message: "Failed to update catachphrase" };
        }
    } catch (err) {
        return { success: false, message: err.message };
    }
}

async function removeCatchphrase(id) {
    let catchphrase;
    try {
        catchphrase = await Catchphrase.findById(id);
        if (catchphrase == null) {
            return { success: false, message: 'Cannot find catchphrase' };
        }

        try {
            await catchphrase.remove()
            return {
                success: true,
                message: 'Deleted Catchphrase'
            };
        } catch (err) {
            return { success: false ,message: err.message };
        }
    } catch (err) {
        return { success: false, message: err.message };
    }
}

module.exports = {
    getAllCatchphrases,
    getCatchphraseById,
    addCatchphrase,
    updateCatchphrase,
    removeCatchphrase
}
```

The controller file will contain the logic used to query our database.

## **Creating the Catchphrases Route**

Let’s create a new folder `routes` in the root folder of our application for keeping all the routes.

Create a new file `catchphrases.js` inside the routes folder with the following contents:

```javascript
const express = require('express');
const router = express.Router();
let { getAllCatchphrases, getCatchphraseById, addCatchphrase, updateCatchphrase, removeCatchphrase } = require('../controllers/catchphraseController')

router.get('/', async (req, res) => {
    let response = await getAllCatchphrases(req.query.s, req.query.page, req.query.limit);
    if (response.success == true) {
        res.status(200).json(response);
    } else {
        res.status(404).json(response);
    }
});

router.get('/:id', async (req, res) => {
    let response = await getCatchphraseById(req.params.id);
    res.json(response);
});

router.post('/', async (req, res) => {
    let body = {
        movieName: req.body.movieName,
        catchphrase: req.body.catchphrase,
        movieContext: req.body.movieContext,
    };
    let response = await addCatchphrase(body);

    if (response.success == true) {
        res.status(201).json(response);
    } else {
        res.status(404).json(response);
    }
});

router.put('/:id', async (req, res) => {
    let movieName = null, catchphrase = null, movieContext = null;
    if (req.body.movieName) {movieName = req.body.movieName}
    if (req.body.catchphrase) {catchphrase = req.body.catchphrase}
    if (req.body.movieContext) {movieContext = req.body.movieContext}
    let response = await updateCatchphrase(req.params.id, movieName, catchphrase, movieContext);

    if (response.success == true) {
        res.status(201).json(response);
    } else {
        res.status(404).json(response);
    }
});

router.delete('/:id', async (req, res) => {
    let response = await removeCatchphrase(req.params.id)
    try {
        res.status(200).json(response);
    } catch (err) {
        res.status(500).json(response);
    }
});

module.exports = router;
```

Create a new file `index.js` inside the routes folder with the following contents:

```javascript
const catchphrases = require('./catchphrases')

module.exports = {
    catchphrases
}
```

In this file, we will import all of the routes we create. This will allow us to import this file into our `server.js` to define our routes.

Modify the `server.js` file as follows:

```javascript
const express = require('express');
const connectDb = require("./config/db");
const { catchphrases } = require("./routes/index");

const app = express();
connectDb();

app.use(express.json());

app.use('/catchphrases', catchphrases)

app.listen(process.env.PORT || 5000, () => console.log('Up and running 🚀'));
```

After running the application you should be able to navigate to the following route `localhost:5000/catchphrases` to see all the catchphrases in your database (if there are any 😉)

## **Adding Swagger documentation**

Swagger allows us to auto-document our API. Let's start by installing the following packages:

```bash
npm install --save swagger-ui-express swagger-jsdoc@6.0.0
```

Next, change the `server.js` file accordingly:

```javascript
const express = require('express');
const connectDb = require("./config/db");
const { catchphrases } = require("./routes/index");
const swaggerJsDoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const app = express();
connectDb();

app.use(express.json());

const swaggerOptions = {
    swaggerDefinition: {
        info: {
            title: 'Catchphrases REST API',
            description: "A REST API built with Express and MongoDB. This API provides movie catchphrases and the context of the catchphrase in the movie."
        },
    },
    apis: ["./routes/catchphrases.js"]
}

app.use('/catchphrases', catchphrases)

const swaggerDocs = swaggerJsDoc(swaggerOptions);
app.use('/', swaggerUi.serve, swaggerUi.setup(swaggerDocs));

app.listen(process.env.PORT || 5000, () => console.log('Up and running 🚀'));
```

Next, we need to describe our routes. Change the `catchphrases.js` file located in the routes folder accordingly:

```javascript
const express = require('express');
const router = express.Router();
let { getAllCatchphrases, getCatchphraseById, addCatchphrase, updateCatchphrase, removeCatchphrase } = require('../controllers/catchphraseController')

/**
 * @swagger
 * /catchphrases:
 *   get:
 *     description: All catchphrases
 *     responses:
 *       200:
 *         description: Returns all the catachphrases
 */
router.get('/', async (req, res) => {
    let response = await getAllCatchphrases(req.query.s, req.query.page, req.query.limit);
    if (response.success == true) {
        res.status(200).json(response);
    } else {
        res.status(404).json(response);
    }
});

/**
 * @swagger
 * /catchphrases/{id}:
 *   get:
 *     parameters:
 *      - in: path
 *        name: id
 *        required: true
 *        type: string
 *        description: The catchphrase ID.
 *     description: Get a catchphrase by id
 *     responses:
 *       200:
 *         description: Returns the requested catachphrase
 */
router.get('/:id', async (req, res) => {
    let response = await getCatchphraseById(req.params.id);
    res.json(response);
});

/**
 * @swagger
 * /catchphrases:
 *   post:
 *     parameters:
 *      - in: body
 *        name: catchphrase
 *        description: New catchphrase
 *        schema:
 *          type: object
 *          properties:
 *            movieName:
 *              type: string
 *            catchphrase:
 *              type: string
 *            movieContext:
 *              type: string
 *     responses:
 *       201:
 *         description: Created
 */
router.post('/', async (req, res) => {
    let body = {
        movieName: req.body.movieName,
        catchphrase: req.body.catchphrase,
        movieContext: req.body.movieContext,
    };
    let response = await addCatchphrase(body);

    if (response.success == true) {
        res.status(201).json(response);
    } else {
        res.status(404).json(response);
    }
});

/**
 * @swagger
 * /catchphrases/{id}:
 *   patch:
 *     parameters:
 *      - in: path
 *        name: id
 *        required: true
 *        type: string
 *        description: The catchphrase ID.
 *      - in: body
 *        name: catchphrase
 *        description: Update catchphrase
 *        schema:
 *          type: object
 *          properties:
 *            movieName:
 *              type: string
 *            catchphrase:
 *              type: string
 *            movieContext:
 *              type: string
 *     responses:
 *       201:
 *         description: Created
 */
router.put('/:id', async (req, res) => {
    let movieName = null, catchphrase = null, movieContext = null;
    if (req.body.movieName) {movieName = req.body.movieName}
    if (req.body.catchphrase) {catchphrase = req.body.catchphrase}
    if (req.body.movieContext) {movieContext = req.body.movieContext}
    let response = await updateCatchphrase(req.params.id, movieName, catchphrase, movieContext);

    if (response.success == true) {
        res.status(201).json(response);
    } else {
        res.status(404).json(response);
    }
});

/**
 * @swagger
 * /catchphrases/{id}:
 *   delete:
 *     parameters:
 *      - in: path
 *        name: id
 *        required: true
 *        type: string
 *        description: The catchphrase ID.
 *     description: Delete a catchphrase by id
 *     responses:
 *       200:
 *         description: Returns the requested catachphrase
 */
router.delete('/:id', async (req, res) => {
    let response = await removeCatchphrase(req.params.id)
    try {
        res.status(200).json(response);
    } catch (err) {
        res.status(500).json(response);
    }
});

module.exports = router;
```

After running the application you should be able to navigate to the following route `localhost:5000` to see the documentation generated by Swagger.

## **Hosting on Heroku**

Heroku allows you to host your application free of charge but with limited resources. To set up the project use the following webpage from the [official Heroku documentation](https://devcenter.heroku.com/articles/getting-started-with-nodejs?singlepage=true).

Note: You might need to add the following config vars to run the application:

```plaintext
MONGO_URI = <Your mongo uri>

NODE_ENV = production

NPM_CONFIG_PRODUCTION = false
```

## **Extra**

This is the dataset that I used for this API.

[Movie Catchphrases Dataset](https://www.kaggle.com/thomaskonstantin/150-famous-movie-catchphrases-with-context)

## **Thanks for reading**

This is a very basic example of a REST API built with Node.js/Express and MongoDB. The code can be downloaded from [GitHub](https://github.com/MikeFMeyer/catchphrase-rest-api).