18. Searching For Users with an Aggregate Pipeline and Query Parameters


In this tutorial we’ll create a new endpoint that allows users to search for other users by their username.  The endpoint will use the GET method, /users path, and search, skip, limit, and sortBy query parameters.

An example url is:

GET /users?search=Joe&skip=10&limit=10&sortBy=email:asc
 

The endpoint uses an aggregate pipeline which defines, within an array of objects, a sequence of stages.  Each stage specifies a stage operator and a configuration object which identifies what sort of computation will occur during the stage.  Each stage produces a sequence of document which is passed to the next stage.

The stage operators that we’ll use are $match, $project, $skip, $limit, and $sort.

The $match stage will produce a sequence of documents that match our search criteria.  The pipeline will pass that sequence to the $project stage which will strip out the fields that we don’t want to send to the client.  The new sequence of documents will be passed to the $skip stage which will remove from the top of the sequence a specified number of documents (perhaps those that have already been read by the user).  The $limit stage will keep only a specified number of documents (from the top) which will be sent to the client.  And last, the $sort stage will reorder the documents according to the sortBy query parameter.

Create the Endpoint

Add the following code to your user router.

router.get('/users', auth, async (req, res) => {

    console.log(`GET users query: ${JSON.stringify(req.query ?? "no query")}`)

    let search = req.query?.search ?? ''
    const escapedTerm = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

    const filter = { username: { $regex: escapedTerm, $options: 'i' } }

    const pipeline = User.aggregate([
        { $match: filter },
        { $project: {
                "_id": 1,
                "username": 1,
                "email": 1
           } 
        }
    ])

    if (req.query.skip) {
        pipeline.append({ $skip: parseInt(req.query.skip) })
    }

    if (req.query.limit) {
        pipeline.append({ $limit: parseInt(req.query.limit) })
    }

    if (req.query.sortBy) {
        const parts = req.query.sortBy.split(':')
        const sort = {}
        sort[parts[0]] = (parts[1] === 'asc') ? 1 : -1
        pipeline.append({ $sort: sort })
    }

    try {
        const users = await pipeline.exec()
        const total = await User.countDocuments(filter)

        res.send({ users, total })
        return
    }
    catch (e) {
        console.log(e)
        res.status(500).send()
    }

})

Let’s walk through the code.

On line 1 we specify the method and path, include the auth middleware, and begin the handler.

Line 3 shows (by printing the query parameters to the console) that they all end up on the req.query object.

On line 5 we grab the search query parameter, if there is one, and if not we set search to the empty string.

Next, on line 6, we escape (add \ before)  all special characters.  For example, if search contains ‘moe.’ then escapedTerm would hold ‘moe\.‘.  This makes $match match the literal value of any special character – not utilize the special character’s regex meaning.

Line 8 creates a filter object that we’ll use to match documents.  The filter specifies (using $regex)  that we want to match all documents that have the escapedTerm as a substring (ignoring the case) in their username field. 

Line 10 creates a pipeline by passing an array of objects (each a stage) to User.aggregate().

Line 11, defines the first stage, the $match stage and lines 12-16 define a projection which specifies which fields we want to keep in the documents as the documents pass through the stages.

One lines 20-22, if the query parameters have a skip key, then we add a $skip stage and define the number of documents to skip.  This (along with limit) allows us to implement pagination.  

If the query parameters include a limit key, then on lines 24-26 we add a $limit stage and define the number of documents to return.

And if the query parameters include a sortBy key, then on lines 28-33 we add a $sort stage.  The value assigned to $sort is an object with one property whose key is the name of the field on which to sort and whose value is 1 if we wish to sort the documents in ascending order, or -1 if descending.

On line 36 we execute the pipeline to obtain a set of documents to return to the client.

One line 37 we pass the same filter we used in the $match stage to countDocuments() which counts how many total documents there are in the collection that match the filter.  We pass this count back to the client as well so the client can determine whether or not to show the user a more button.

Test the Endpoint with Postman

Create a new request in Postman for the new endpoint.  You’ll likely have to create more users in your database in order to adequately test the endpoint.

Push to GitHub

Git add, git commit, and git push.