I just announced the new Spring 5 modules in REST With Spring:


Table of Contents

1. Overview

This tutorial will focus on the implementation of pagination in a REST API, using Spring MVC and Spring Data.

2. Page as Resource vs Page as Representation

The first question when designing pagination in the context of a RESTful architecture is whether to consider the page an actual Resource or just a Representation of Resources.

Treating the page itself as a resource introduces a host of problems such as no longer being able to uniquely identify resources between calls. This, coupled with the fact that, in the persistence layer, the page is not proper entity but a holder that is constructed when necessary, makes the choice straightforward: the page is part of the representation.

The next question in the pagination design in the context of REST is where to include the paging information:

  • in the URI path: /foo/page/1
  • the URI query: /foo?page=1

Keeping in mind that a page is not a Resource, encoding the page information in the URI is no longer an option.

We are going to use the standard way of solving this problem by encoding the paging information in a URI query.

3. The Controller

Now, for the  implementation – the Spring MVC Controller for pagination is straightforward:

@RequestMapping( value = "admin/foo",params = { "page", "size" }, method = GET )
public List< Foo > findPaginated(
 @RequestParam( "page" ) int page, @RequestParam( "size" ) int size,
 UriComponentsBuilder uriBuilder, HttpServletResponse response ){

   Page< Foo > resultPage = service.findPaginated( page, size );
   if( page > resultPage.getTotalPages() ) {
      throw new ResourceNotFoundException();
   eventPublisher.publishEvent( new PaginatedResultsRetrievedEvent< Foo >
    ( Foo.class, uriBuilder, response, page, resultPage.getTotalPages(), size ) );

   return resultPage.getContent();

The two query parameters are injected into the Controller method via @RequestParam.

We’re also injecting both the Http Response and the UriComponentsBuilder to help with Discoverability – which we are decoupling via a custom event. If that is not a goal of the API, you can simply remove the custom event and be done.

Finally – note that the focus of this article is only the REST and the web layer – to go deeper into the data access part of pagination you can check out this article about Pagination with Spring Data.

4. Discoverability for REST Pagination

Within the scope of pagination, satisfying the HATEOAS constraint of REST means enabling the client of the API to discover the next and previous pages based on the current page in the navigation. For this purpose, we’re going to use the Link HTTP header, coupled with the officialnext“, “prev“, “first” and “last” link relation types.

In REST, Discoverability is a cross-cutting concern, applicable not only to specific operations but to types of operations. For example, each time a Resource is created, the URI of that Resource should be discoverable by the client. Since this requirement is relevant for the creation of ANY Resource, it should be dealt with separately and decoupled from the main Controller flow.

With Spring, this decoupling is done with Events, as was thoroughly discussed in the previous article focusing on Discoverability of a REST Service. In the case of pagination, the event – PaginatedResultsRetrievedEvent – is fired in the controller layer, and discoverability is implemented with a custom listener for this event:

void addLinkHeaderOnPagedResourceRetrieval(
 UriComponentsBuilder uriBuilder, HttpServletResponse response,
 Class clazz, int page, int totalPages, int size ){

   String resourceName = clazz.getSimpleName().toString().toLowerCase();
   uriBuilder.path( "/admin/" + resourceName );

   StringBuilder linkHeader = new StringBuilder();
   if( hasNextPage( page, totalPages ) ){
      String uriNextPage = constructNextPageUri( uriBuilder, page, size );
      linkHeader.append( createLinkHeader( uriNextPage, "next" ) );
   if( hasPreviousPage( page ) ){
      String uriPrevPage = constructPrevPageUri( uriBuilder, page, size );
      appendCommaIfNecessary( linkHeader );
      linkHeader.append( createLinkHeader( uriPrevPage, "prev" ) );
   if( hasFirstPage( page ) ){
      String uriFirstPage = constructFirstPageUri( uriBuilder, size );
      appendCommaIfNecessary( linkHeader );
      linkHeader.append( createLinkHeader( uriFirstPage, "first" ) );
   if( hasLastPage( page, totalPages ) ){
      String uriLastPage = constructLastPageUri( uriBuilder, totalPages, size );
      appendCommaIfNecessary( linkHeader );
      linkHeader.append( createLinkHeader( uriLastPage, "last" ) );
   response.addHeader( "Link", linkHeader.toString() );

In short, the listener checks if the navigation allows for a next, previous, first and last pages and – if it does – adds the relevant URIs to the Link HTTP Header.

Note that, for brevity, I included only a partial code sample and the full code here.

5. Test Driving Pagination

Both the main logic of pagination and discoverability are covered by small, focused integration tests; as in the previous article, the rest-assured library is used to consume the REST service and to verify the results.

These are a few example of pagination integration tests; for a full test suite, check out the GitHub project (link at the end of the article):

public void whenResourcesAreRetrievedPaged_then200IsReceived(){
   Response response = givenAuth().get( paths.getFooURL() + "?page=0&size=2" );

   assertThat( response.getStatusCode(), is( 200 ) );
public void whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived(){
   String url = getFooURL() + "?page=" + randomNumeric(5) + "&size=2";
   Response response = givenAuth().get(url);

   assertThat( response.getStatusCode(), is( 404 ) );
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources(){

   Response response = givenAuth().get( paths.getFooURL() + "?page=0&size=2" );

   assertFalse( response.body().as( List.class ).isEmpty() );

6. Test Driving Pagination Discoverability

Testing that pagination is discoverable by a client is relatively straightforward, although there is a lot of ground to cover. The tests are focused on the position of the current page in navigation and the different URIs that should be discoverable from each position:

public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext(){
   Response response = givenAuth().get( getFooURL()+"?page=0&size=2" );

   String uriToNextPage = extractURIByRel( response.getHeader( "Link" ), "next" );
   assertEquals( getFooURL()+"?page=1&size=2", uriToNextPage );
public void whenFirstPageOfResourcesAreRetrieved_thenNoPreviousPage(){
   Response response = givenAuth().get( getFooURL()+"?page=0&size=2" );

   String uriToPrevPage = extractURIByRel( response.getHeader( "Link" ), "prev" );
   assertNull( uriToPrevPage );
public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious(){
   Response response = givenAuth().get( getFooURL()+"?page=1&size=2" );

   String uriToPrevPage = extractURIByRel( response.getHeader( "Link" ), "prev" );
   assertEquals( getFooURL()+"?page=0&size=2", uriToPrevPage );
public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable(){
   Response first = givenAuth().get( getFooURL()+"?page=0&size=2" );
   String uriToLastPage = extractURIByRel( first.getHeader( "Link" ), "last" );

   Response response = givenAuth().get( uriToLastPage );

   String uriToNextPage = extractURIByRel( response.getHeader( "Link" ), "next" );
   assertNull( uriToNextPage );

Note that the full low-level code for extractURIByRel – responsible for extracting the URIs by rel relation is here.

7. Getting All Resources

On the same topic of pagination and discoverability, the choice must be made if a client is allowed to retrieve all the Resources in the system at once, or if the client MUST ask for them paginated. If the choice is made that the client cannot retrieve all Resources with a single request, and pagination is not optional but required, then several options are available for the response to a get all request. One option is to return a 404 (Not Found) and use the Link header to make the first page discoverable:

Link=<http://localhost:8080/rest/api/admin/foo?page=0&size=2>; rel=”first“, <http://localhost:8080/rest/api/admin/foo?page=103&size=2>; rel=”last

Another option is to return redirect – 303 (See Other) – to the first page. A more conservative route would be to simply return to the client a 405 (Method Not Allowed) for the GET request.

8. REST Paging with Range HTTP headers

A relatively different way of implementing pagination is to work with the HTTP Range headersRange, Content-Range, If-Range, Accept-Ranges – and HTTP status codes – 206 (Partial Content), 413 (Request Entity Too Large), 416 (Requested Range Not Satisfiable). One view on this approach is that the HTTP Range extensions were not intended for pagination and that they should be managed by the Server, not by the Application. Implementing pagination based on the HTTP Range header extensions is nevertheless technically possible, although not nearly as common as the implementation discussed in this article.

9. Conclusion

This tutorial illustrated how to implement Pagination in a REST API using Spring, and discussed how to set up and test Discoverability.

If you want to go in depth on pagination in the persistence level, check out my JPA or Hibernate pagination tutorials.

The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven-based project, so it should be easy to import and run as it is.

I just announced the new Spring 5 modules in REST With Spring:


newest oldest most voted
Notify of
Tommy McGuire

What do you mean by “This approach does however have one downside – it cuts into the query space for actual queries…”? Are you referring to the size of the query string on the URL, or the number of query parameters?

Eugen Paraschiv

I am mainly referring to the fact that it adds to the length of the query part of the URL – not a major problem.


Using the query string for paging can also cause problems with caching.

Eugen Paraschiv

Agreed – this may make some types of caching mechanism more difficult – but we are dealing with lists of resources here, so presumably these will change anyhow – this makes caching is a smaller concern.
Thanks. Eugen.

Hi, what about collections that can be seen from different point of views ? Eg: Event and Person both have Tickets{person,event,+data}. In this case also there is almost no sense in seeing all tickets of all events. When I start from a Ticket how can I link both the collections ? With a compound rel ? “collection-person” “collection-event” ? Also I had a problem with LINK HTTP header for its size limit..I found myself having to check and skip produced header when search forms are involved and query string gets long, also if tomcat has 8K as default limit I… Read more »
Eugen Paraschiv
Using custom rel types and defining your own custom media type is one way to go, and that would indeed enable you to use these types of link relations. Another is to just use the Link header – it’s not as semantically rich, but it’s standard, so any client will implicitly understand it. So – you could simply point to the URI of the list of tickets via a Link. As for loading up the header with so much information – it’s probably better to treat that as a code smell and try to improve the your responses instead of… Read more »

Hello, I’m running your “spring-security-rest-full” code on eclipse, but it gives me error:

You need to run build with JDK or have tools.jar on the classpath.If this occures during eclipse build make sure you run eclipse under JDK as well (com.mysema.maven:apt-maven-plugin:1.1.3:process:default:generate-sources).

I am looking to implement Pagination with HATEOAS and ResourceAssembler class. Please help me. Provide code.

Eugen Paraschiv

You’re probably running on a fresh workspace in Eclipse, and it picked up your JRE instead of your JDK. Go into Preferences -> Java -> Installed JREs and add your JDK in there (replacing the JRE). Hope that helps. Cheers,