Strategies for versioning REST services
Adding pagination capabilities
Adding sorting capabilities
We all are familiar with the famous proverb “The only thing constant in life is change.” This applies to software development. In this chapter, we will look at versioning our API as a way to handle such changes. Additionally, dealing with large datasets can be problematic especially when mobile clients are involved. Large datasets can also result in server overload and performance issues. To handle this, we will employ paging and sorting techniques and send data in manageable chunks.
Versioning
As user requirements and technology change, no matter how planned our design is, we will end up changing our code. This will involve making changes to REST resources by adding, updating, and sometimes removing attributes. Although the crux of the API—read, create, update, and remove one or more resources—remains the same, this could result in such drastic changes to the representation that it may break any existing consumers. Similarly, changes to functionality such as securing our services and requiring authentication or authorization can break existing consumers. Such major changes typically call for new versions of the API.
In this chapter, we will be adding paging and sorting functionality to our QuickPoll API. As you will see in later sections, this change will result in changes to the representations returned for some of the GET HTTP methods. Before we version our QuickPoll API to handle paging and sorting, let’s look at some approaches for versioning.
Versioning Approaches
URI versioning
URI parameter versioning
Accept header versioning
Custom header versioning
None of these approaches are silver bullets, and each has its fair share of advantages and disadvantages. In this section we will look at these approaches along with some real-world public APIs that use them.
URI Versioning
In this approach, version information becomes part of the URI. For example, http://api.example.org/v1/users and http://api.example.org/v2/users represent two different versions of an application API. Here we use v notation to denote versioning, and the numbers 1 and 2 following the v indicate the first and second API versions.
As you can see, LinkedIn, Yahoo, and SalesForce use the v notation. In addition to a major version, SalesForce uses a minor version as part of its URI version. Twilio, by contrast, takes a unique approach and uses a timestamp in the URI to differentiate its versions.
Making a version part of the URI is very appealing as the version information is right in the URI. It also simplifies API development and testing. Folks can easily browse and use different versions of REST services via a web browser. On the contrary, this might make client’s life difficult. For example, consider a client storing references to user resources in its database. On switching to a new version, these references get outdated and the client has to do a mass database update to upgrade references to new version.
URI Parameter Versioning
This is similar to the URI versioning that we just looked at except that the version information is specified as a URI request parameter. For example, the URI http://api.example.org/users?v=2 uses the version parameter v to represent the second version of the API. The version parameter is typically optional, and a default version of the API will continue working for requests without version parameter. Most often, the default version is the latest version of the API.
Although as not popular as other versioning strategies, a few major public APIs such as Netf lix have used this strategy. The URI parameter versioning shares the same disadvantages of URI versioning. Another disadvantage is that some proxies don’t cache resources with a URI parameter, resulting in additional network traffic.
Accept Header Versioning
This versioning approach uses the Accept header to communicate version information. Because the header contains version information, there will be only one URI for multiple versions of API.
The vnd is the starting point of the custom media type and indicates vendor. The product or producer name is the name of the product and distinguishes this media type from other custom product media types. The version part is represented using strings such as v1 or v2 or v3. Finally, the suffix is used to specify the structure of the media type. For example, the +json suffix indicates a structure that follows the guidelines established for media type "application/json." RFC 6389 (https://tools.ietf.org/html/rfc6839) gives a full list of standardized prefixes such as +xml, +json, and +zip. Using this approach, a client, for example, can send an application/vnd.quickpoll.v2+json accept header to request the second version of the API.
The Accept header versioning approach is becoming more and more popular as it allows fine-grained versioning of individual resources without impacting the entire API. This approach can make browser testing harder as we have to carefully craft the Accept header. GitHub is a popular public API that uses this Accept header strategy. For requests that don’t contain any Accept header, GitHub uses the latest version of the API to fulfill the request.
Custom Header Versioning
This approach shares the same pros and cons as that of the Accept header approach. Because the HTTP specification provides a standard way of accomplishing this via the Accept header, the custom header approach hasn’t been widely adopted.
Deprecating an API
As you release new versions of an API, maintaining older versions becomes cumbersome and can result in maintenance nightmares. The number of versions to maintain and their longevity depend on the API user base, but it is strongly recommended to maintain at least one older version.
API versions that will no longer be maintained need to be deprecated and eventually retired. It is important to remember that deprecation is intended to communicate that the API is still available but will cease to exist in the future. API users should be given plenty of notices about deprecation so that they can migrate to newer versions.
QuickPoll Versioning
In this book, we will be using the URI versioning approach to version the QuickPoll REST API.
Complete code replication—In this approach, you replicate the entire code base and maintain parallel code paths for each version. Popular API builder Apigility takes this approach and clones the entire code base for each new version. This approach makes it easy to make code changes that wouldn’t impact other versions. It also makes it easy to switch backend datastores. This would also allow each version to become a separate deployable artifact. Although this approach provides a lot of flexibility, we will be duplicating the entire code base.
Version-specific code replication—In this approach, we only replicate the code that is specific to each version. Each version can have its own set of controllers and request/response DTO objects but will reuse most of the common service and backend layers. For smaller applications, this approach can work well as version-specific code can simply be separated into different packages. Care must be taken when making changes to the reused code, as it might have impact on multiple versions.
Spring MVC makes it easy to version a QuickPoll application using the URI versioning approach. Considering that versioning plays a crucial role in managing changes, it is important that we version as early as possible in the development cycle. Hence, we will assign a version (v1) to all of the QuickPoll services that we have developed so far. To support multiple versions, we will follow the second approach and create a separate set of controllers.
In this chapter we will continue building on the work that we did on the QuickPoll application in the previous chapters. Alternatively, a starter project inside the Chapter7starter folder of the downloaded source code is available for you to use. The completed solution is available under the Chapter7final folder. Please refer to this solution for complete listings containing getters/setters and additional imports. The downloaded Chapter7 folder also contains an exported Postman collection containing REST API requests associated with this chapter.
We begin the versioning process by creating two packages com.apress.v1.controller and com.apress.v2.controller. Move all of the controllers from the com.apress.controller package to the com.apress.v1.controller. To each controller in the new v1 package, add a class-level @RequestMapping ("/v1") annotation. Because we will have multiple versions of controllers, we need to give unique component names to individual controllers. We will follow the convention of appending version number to the unqualified class name to derive our component name. Using this convention, the v1 PollController will have a component name pollControllerV1.
Version 1 of the Poll Controller
Even though the behavior and code of VoteController and ComputeResultControler don’t change across versions, we are copying the code to keep things simple. In real-world scenarios, refactor code into reusable modules, or use inheritance to avoid code duplication.
With the class-level @RequestMapping annotation in place, all of the URIs in the v1 PollController become relative to "/v1/." Restart the QuickPoll application, and, using Postman, verify that you can create a new Poll at the new http://localhost:8080/v1/polls endpoint.
Version 2 of the Poll Controller
Once you have completed modifications for the three controllers, restart the QuickPoll application, and, using Postman, verify that you can create a new poll using the http://localhost:8080/v2/polls endpoint. Similarly, verify that you can access the VoteController and ComputeResultController endpoints by accessing the http://localhost:8080/v2/votes and http://localhost:8080/v2/computeresult endpoints.
SwaggerConfig
Refactored SwaggerConfig Class
This concludes the configuration needed to version our QuickPoll application and sets the stage for adding pagination and sorting support in the final two sections of this chapter.
Pagination
REST APIs are consumed by a variety of clients ranging from desktop applications to Web to mobile devices. Hence, while designing a REST API capable of returning vast datasets, it is important to limit the amount of data returned for bandwidth and performance reasons. The bandwidth concerns become more important in the case of mobile clients consuming the API. Limiting the data can vastly improve the server’s ability to retrieve data faster from a datastore and the client’s ability to process the data and render the UI. By splitting the data into discrete pages or paging data, REST services allow clients to scroll through and access the entire dataset in manageable chunks.
Before we start implementing pagination in our QuickPoll application, let’s look at four different pagination styles: page number pagination, limit offset pagination, cursor-based pagination, and time-based pagination.
Page Number Pagination
Limit Offset Pagination
Cursor-Based Pagination
On receiving this request, the service would send the data along with the prev and next cursor fields. This pagination style is used by applications such as Twitter and Facebook that deal with real-time datasets (tweets and posts) where data changes frequently. The generated cursors typically don’t live forever and should be used for short-term pagination purposes only.
Time-Based Pagination
Both examples use the limit parameter to indicate the maximum number of items to be returned. The until parameter specifies the end of the time range, whereas the since parameter specifies the beginning of the time range.
Pagination Data
QuickPoll Pagination
To support large poll datasets in a QuickPoll application, we will be implementing the page number pagination style and will include the paging information in the response body.
We begin the implementation by configuring our QuickPoll application to load dummy poll data into its database during the bootstrapping process. This would enable us to test our polling and sorting code. To achieve this, copy the import.sql file from the downloaded chapter code into srcmain esources folder. The import.sql file contains DML statements for creating test polls. Hibernate out of the box loads the import.sql file found under the classpath and executes all of the SQL statements in it. Restart the QuickPoll application, and navigate to http://localhost:8080/v2/polls in Postman; it should list all of the loaded test polls.
Spring Data JPA’s Paging and Sorting Repository
Total elements—Total elements in the result set
Number of elements—Number of elements in the returned subset
Size—The maximum number of elements in each page
Total pages—Total number of pages in the result set
Number—Returns the current page number
Last—Flag indicating if it is the last data subset
First—Flag indicating if it is the first data subset
Sort—Returns parameters used for sorting, if any
PollRepository Implementation
GetAllPolls Method with Paging Functionality
Spring Data JPA uses a zero index–based paging approach. Hence, the first page number starts with 0 and not 1.
Changing Default Page Size
Spring MVC uses an org.springframework.data.web.PageableHandlerMethodArgumentResolver to extract paging information from the request parameters and inject Pageable instances into Controller methods. Out of the box, the PageableHandlerMethodArgumentResolver class sets the default page size to 20. Hence, if you perform a GET request on http://localhost:8080/v2/polls, the response would include 20 polls. Although 20 is a good default page size, there might be occasions when you might want to change it globally in your application. To do this, you need to create and register a new instance of PageableHandlerMethodArgumentResolver with the settings of your choice.
Code to Change Default Page Size to 5
Paging Metadata for Default Page Size 5
Sor ting
Sorting allows REST clients to determine the order in which items in a dataset are arranged. REST services supporting sorting allow clients to submit parameters with properties to be used for sorting. For example, a client can submit the following request to sort blog posts based on their created date and title:
http://blog.example.com/posts?sort=createdDate,title
Sort Ascending or Sort Descending
In the first approach, the sort parameter clearly specifies if the direction should be ascending or descending.
In the second approach, we have used the same parameter name for both directions. However, the parameter value spells out the sort direction.
The last approach uses the “-” notation to indicate that any property prefixed with a “-” should be sorted on a descending direction. Properties that are not prefixed with a “-” will be sorted in the ascending direction.
QuickPoll Sorting
Considering that sorting is typically used in conjunction with paging, Spring Data JPA’s PagingAndSortingRepository and Pageable implementations are designed to handle and service sorting requests from the ground up. Hence, we don’t require any explicit implementation for sorting.
Summary
In this chapter we reviewed the different strategies for versioning REST API. We then implemented versioning in QuickPoll using the URL versioning approach. We also reviewed the different approaches for dealing with large datasets using pagination and sorting techniques. Finally, we used Spring Data’s out-of-the-box functionality to implement page number pagination style. In the next chapter, we will review strategies for securing REST services.