Aquasar
  • Home
  • Portfolio
  • Articles
  • Pricing
  • About
  • Contact
WEB DEVELOPMENT |SEO |DIGITAL ADS

Using Express with Mongoose

Aug 16th, 2019

Alex Quasar

mongoapis with mongoose

Overview

This post will assume that your comfortable or familiar with crud operations in express and have some familiarity with mongoose. As you know mongoose is an layer of abstraction or superset that sits on top of mongoDb, similar to how jQuery is a layer of abstraction sitting on top of javascript.

In the last section we create a Task model schema and in this section we will add a few more items to that model to give some examples of how we can make a more advanced API.

Starting Examples:

Here is a starting example, that will get all the results in the Tour model using the find method with no parameters.

// Type         :   GET
// Route        :   /api/tours
// Description  :   Get all the tours
// Access       :   Public - anyone can access.
router.get('/', async (req, res) => {
    try {
        const tour = await Tour.find();
  
          // send back tours array.
        res.json(tour)

    } catch (error) {
        res.status(400).json({msg: `There as an error with the /api/tours route`,error})
    }
})
 

To set up filtering, we need the pass some arguments as key/value pairs to the find method. For example the tour document now becomes:

  const tour = await Tour.find({
            duration: 5,
            difficulty: 'easy'
        }); 

Alternatively using mongoose methods we can do something like:

const tour = await Tour.find()
            .where('duration').equals(5)
            .where('difficulty').equals('easy')  

The benefit of doing it this way is when we want to do some advanced filtering and take advantage of the built in mongoose methods like gt, gte, lt, lte.

For example:

const tour = await Tour.find()
            .where('duration').gt(5)

Or alternatively:

const tour = await Tour.find({duration: {$gt:10}}) 

Using req.query:

Using req.query in the find method allows us to parse our query strings from the url. For example, in postman make the following request to an endpoint with query strings like that below

http://localhost:5000/api/tours?duration=5&difficulty=easy

Now to find, ie. return only the data with those parameters simply change the tour document to:

const tour = await Tour.find(req.query) 

Modifying our Express API.

When you go to a website like amazon.com or indeed.com you often see more advanced query strings that implement sort and pagination. Since we don't want to query our data for things like page=2 or sort=ASC we need to exclude these and not pass it into the find method.

const queryObj = {...req.query}
        const excludedFields = ['page', 'sort', 'limit', 'fields'];
        excludedFields.forEach( el => delete queryObj[el] );
        const tour = await Tour.find(queryObj)

This little trick enables us to exclude the page, sort, limit and fields query strings to the find method.

Applying Greater than or Less than logic

So far our query strings can only handle simple requests, ie. duration=5. What if we need to handle more advanced filtering. For example when searching on a auto dealership website, we want to see all models between a certain price range.

The query string is could look something like this:

http://localhost:5000/api/tours?duration[gte]=5&price[lte]500

The req.query we get back from the url is almost the same as the query needed to be passed into the mongoose.find method to query the correct data. The only difference is that the query will not have a $ symbol in front of it, which denotes an operator in mongoDb. Using regular expressions we can add in this $ sign in front of the operators.

The steps require using a regular expression to replace certain instances of keywords we want to look.

Step 1: Turn query object to query string:

  let queryString = JSON.stringify(queryObj)

Step 2: Replace the keywords, lte, lt, gt and gte with the mongoose equivalents.

 // replace gte, gt, lte, lt => $gte, $gt, $lte, $lt
        queryString = queryString.replace(/\b(gte|gt|lt|lte)\b/g, match => `$${match}`);

Step 3: Pass in this new queryString as an object to be filtered by the find method

const tour = await Tour.find(JSON.parse(queryString));

Sorting

For sorting, we can do the following. First will store the result into an intermediate variable, query and then return the final tour document as shown below:

let query = Tour.find(JSON.parse(queryString));

        // Sorting
        if( req.query.sort ) {
             query.sort(req.query.sort);
        }
        const tour = await query;
          // send back tours array.
        res.json(tour) 

In postman, we can then make the following GET request:

http://localhost:5000/api/tours?sort=-ratingsAverage

which will sort the data by ratingsAverage in a descending order

Sorting multiple parameters

We can improve the sort functionality by allowing the user to sort by multiple parameters in the case there is a tie.

To handle the following request from postman:

http://localhost:5000/api/tours?sort=-ratingsAverage,name 

In mongoose, we can specify multiple sorts by passing in as many parameters as we like using spaces. Since the url does not spaces, but commas separating the sorting parameters we can fix this by.

// Sorting
   if( req.query.sort ) {
       const sortBy = req.query.sort.split(',').join(' ');
        query.sort(sortBy);
   } 

Selecting only certain fields

To select only certain fields we can use the select method that is available on a mongoose document. The select method in mongoose you specify the fields you want separated by spaces so similar to the sort method we do the following:

// Field limits
        if( req.query.fields) {
            const fields = req.query.fields.split(',').join(' ');
            query.select(fields);

        } 

Note if you want to exclude certain fields than prefix the field name with a - symbol.

To hide a specific field, we can also go into the model and use: select: false option.

Pagination

To implement pagination with the following simple formula on line 3. If the number of results to skip is greater than the actual number of results in the document, than we can throw an error.

 // Pagination
        const page = req.query.page * 1 || 1;
        const limit = req.query.limit * 1 || 100;

        const skip = (page - 1) * limit ;
        if( req.query.page ) {
            const numTours = await Tour.countDocuments();
            if( skip >= numTours) {
                throw new Error('This page does not exist!');
            }
        }

        query.skip(skip).limit(limit);
  

Creating a Top 5 query route.

It could be handy to have a top 5 tours route. For this, we can predefine our price, limits and other things that must meet the criteria of a top 5 tour. For example:

// Type         :   GET
// Route        :   /api/tours/top-5-tours
// Description  :   Get all top 5 tours
// Access       :   Public - anyone can access.
router.get('/top-5-tours', async (req,res) => {
    try {
        let query = Tour.find({
            ratingsAverage: {$gte: 4.8}
        });
        query.limit(5).sort('-ratingsAverage -price').select('name price difficulty summary ratingsAverage');
        const tour = await query;
        res.json(tour)
    } catch (error) {
        res.status(400).json({msg: `There as an error with the /api/tours/top-5-tours route`,error})
    }
})