Introduction
Writing truly Restful APIs is not simple. There are a lot of features that need to be covered to call an API restful. One aspect of those features is providing parameters to filter and manipulate the data of a resource. Two months ago I was practicing my TDD skills, and I decided to build a simple blog engine using Spring Boot + Spring Data MongoDB (technologies that I never used before). So I got to a point that I needed to support filtering, selecting, sorting and pagination. I realized there was nothing "simple" that I could use, so I build a custom solution and published it on the Maven Central Repository.
Concept
We will build an endpoint that supports filtering, selecting, sorting and pagination using Spring Boot + Spring Data MongoDB. We will build it in a way that implementing it to other controllers would be straight forward. To support these filters we have to accept a parameter to our controller, so we have to define a RequestParam.
The parameter must be a type of Map which will represent our filters. The valid keys of the map should be sort, select, q, pageSize, page representing our different filters that can be applied to our resource. For example to limit the results of your query you have to add the parameter pageSize with value a number greater than or equal to 0 to the URL.
GET /blogs?pageSize=5
The pageSize parameter will be a key to our RequestParam map variable with a value the number defined to the URL.
There are two steps for implementing this.
- Collect the URL parameters and pass them to the repository.
- Translate the URL parameters in a way MongoDB understands.
Implementation
Let's begin by defining our Rest Controller.
After having our filters as a parameter we have to validate their existence and collect them to pass them to our service and eventually to our repository. This function should be in a separate class from our Rest Controller to be reusable to other controllers as well.
The collectRestApiParams function will accept the filters from a controller and check if there are valid keys defined as a parameter to the URL and if there are it will add them to a new map. It's responsible for collecting all parameters, so for each one, we have a separate function. The collect functions are pretty simple, they just check if there is a valid key in the filter's map and adds it to our new map called restApiQueries.
For example the collectPageFilter checks if there is a key "page" and just adds it to restApiQueries.
The collectRestApiParams will be called from inside our Rest Controller and the returned value will be a map of valid keys to be passed to the service.
The service will delegate these filters to the repository and with that, we finished the first step: Collect the URL parameters and pass them to the repository :heavy_check_mark:
The next step is to translate these filters in a way that MongoDB understands. To do that we will use a Query object and apply MongoDB filters upon it before the database call. Our repository function looks like this:
The last missing part is the functions that are applying these filters to the query object. So let's define the functions that translate the parameters to MongoDB filters. The function applyRestApiQueries is responsible for applying all parameters to filters of the query object and should be to a separate class for reusability purposes.
This query object is received from the repository function before the database call.
Now let's write the functions that apply each parameter separately to the query object.
The applyPageQueryParam takes the value of the key "page" and applies the "skip" filter to our query object by calling query.skip(skip)
.
Because we are passing the query object by reference, any changes made to this object will be affected to the repository's query object as well.
The applyPageSizeQueryParam takes the value of the key "pageSize" and applies the "limit" filter to our query object by calling query.limit(limit)
.
The applySortQueryParam takes the value of the "sort" key which is a field name of a resource that represents the sorting field. If the field has a minus "-" symbol in front of it, it means the direction is descending otherwise it's ascending. After storing its value, we apply the "sort" filter to our query object by calling query.with(new Sort(sortDir, sortBy))
.
The applySelectQueryParam takes the value of the "select" which is a string of field names of a resource separated by a comma. Then split the fields with the comma delimiter and we append each field by using query.fields().include(selectedField)
.
The applySearchQueryParam is the trickiest one. The value of the "q" key in the map is a JSON object, so we have to parse the JSON and pass it to the query by calling query.addCriteria(Criteria.where(field).is(value))
.
The second step is done: Translate the URL parameters in a way MongoDB understands :heavy_check_mark:
Testing time
Here is our testing resource:
Let's try to sort our blogs by title with descending order.
We can select specific fields from the blogs, such as id, title.
We can also limit the results and retrieve the second page.
Note that the page starts from index 0.
Now let's search for a blog with the text "This is content 1".
Conclusion
The implementation is pretty simple, we just added two extra steps to our flow to support the filters, the collection of the parameters which will take place on our Rest Controller, and the filter applier which will take place before the database call.
As I said before I published this code to the Maven Central Repository so you can support filters to your application by just adding the dependency. Read the documentation for more details.
Source code
Available on GitHub.
Blog is also posted in dev.to.
Subscribe to my newsletter
An irregular digest about tech, software, and mentoring.