Skip to main content

Query paginated results

Complete Code
The end result of the code developed in this document can be found in the GitHub monorepo springboot-demo-projects, commit(s) /films endpoint (spring_java), /films endpoint (spring_kotlin), /films endpoint (spring_groovy)

In the previous document we wired a single-film lookup through the hexagonal architecture, from controller to JPA repository and back. This document adds pagination to the /films endpoint so clients can request pages of results instead of loading everything at once.

Files Overview

Files to Create/Modify
File Tree
├── ...
└── src
├── main
│ ├── java
│ │ └── dev
│ │ └── pollito
│ │ └── spring_java
│ │ └── sakila
│ │ └── film
│ │ ├── adapter
│ │ │ ├── in
│ │ │ │ └── rest
│ │ │ │ ├── FilmRestController.java
│ │ │ │ └── FilmRestMapper.java
│ │ │ └── out
│ │ │ └── jpa
│ │ │ ├── FilmJpaMapper.java
│ │ │ ├── FilmJpaRepository.java
│ │ │ └── FilmRepositoryImpl.java
│ │ └── domain
│ │ ├── port
│ │ │ ├── in
│ │ │ │ └── FilmUseCases.java
│ │ │ └── out
│ │ │ └── FilmRepository.java
│ │ └── service
│ │ └── FilmUseCasesImpl.java
│ └── resources
│ ├── application.yaml
│ ├── application-dev.yaml
│ └── openapi.yaml
└── test
└── java
└── dev
└── pollito
└── spring_java
├── sakila
│ └── film
│ ├── adapter
│ │ ├── in
│ │ │ └── rest
│ │ │ └── FilmRestControllerMockMvcTest.java
│ │ └── out
│ │ └── jpa
│ │ └── FilmRepositoryImplDataJpaTest.java
│ └── domain
│ └── service
│ └── FilmUseCasesImplTest.java
└── test
└── util
└── MockMvcResultMatchers.java

Every layer in the architecture changes:

  • The OpenAPI spec gets x-spring-paginated: true on the /films endpoint.
  • FilmUseCases and FilmRepository grow a getFilms(Pageable) method.
  • The JPA repository overrides findAll with an @EntityGraph.
  • The REST mapper gains a PageFilmListResponseAllOfData mapping.
  • The application.yaml files pick up Spring Data's pagination defaults.
  • A shared hasPageFields() test assertion is added to MockMvcResultMatchers.

The OpenAPI spec and x-spring-paginated

The /films endpoint already existed in the OpenAPI spec, but before these commits it threw RuntimeException("Not implemented"). The key addition is x-spring-paginated: true:

openapi.yaml (snippet)
paths:
/films:
get:
x-spring-paginated: true
tags:
- Films
operationId: getFilms

This custom extension tells the OpenAPI generator to resolve the page, size, and sort query parameters into a Spring Pageable object rather than three separate method parameters. Without it, the generated controller interface would look like getFilms(Integer page, Integer size, List<String> sort). With it, the generator produces getFilms(Pageable pageable), and Spring Data's PageableHandlerMethodArgumentResolver automatically parses the query string into a Pageable instance.

springdoc-openapi-starter-webmvc-ui

The build files also add the springdoc-openapi-starter-webmvc-ui:3.0.2 dependency. This serves the OpenAPI spec as a live Swagger UI at /swagger-ui.html, which is useful for ad-hoc exploration. It does not affect the pagination logic itself.

build.gradle (snippet)
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2'

Configure pagination defaults

Spring Data lets you set global defaults for pagination so every request that omits page or size gets predictable values. The three YAML files (application.yaml, application-dev.yaml, application-test.yaml) all add the same block:

resources/application.yaml (snippet)
spring:
data:
web:
pageable:
default-page-size: 10
max-page-size: 100
  • default-page-size: 10 — when the client doesn't include size, return 10 results per page.
  • max-page-size: 100 — cap any request at 100 results per page, so a client can't ask for all 1,000 films in one shot.

Add pagination through the layers

Domain port interfaces

Both the inbound port (FilmUseCases) and the outbound port (FilmRepository) gain a getFilms(Pageable) method that returns Page<Film>:

java/dev/pollito/spring_java/sakila/film/domain/port/in/FilmUseCases.java
package dev.pollito.spring_java.sakila.film.domain.port.in;

import dev.pollito.spring_java.sakila.film.domain.model.Film;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface FilmUseCases {
Film getFilm(Integer id);

Page<Film> getFilms(Pageable pageable);
}
java/dev/pollito/spring_java/sakila/film/domain/port/out/FilmRepository.java
package dev.pollito.spring_java.sakila.film.domain.port.out;

import dev.pollito.spring_java.sakila.film.domain.model.Film;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface FilmRepository {
Film getFilm(Integer id);

Page<Film> getFilms(Pageable pageable);
}

The domain layer only knows about Page<Film> and Pageable. It has zero knowledge of JPA or HTTP.

Use case implementation

FilmUseCasesImpl delegates straight through to the repository — same pattern as getFilm:

java/dev/pollito/spring_java/sakila/film/domain/service/FilmUseCasesImpl.java
package dev.pollito.spring_java.sakila.film.domain.service;

import dev.pollito.spring_java.sakila.film.domain.model.Film;
import dev.pollito.spring_java.sakila.film.domain.port.in.FilmUseCases;
import dev.pollito.spring_java.sakila.film.domain.port.out.FilmRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class FilmUseCasesImpl implements FilmUseCases {
private final FilmRepository repository;

@Override
public Film getFilm(Integer id) {
return repository.getFilm(id);
}

@Override
public Page<Film> getFilms(Pageable pageable) {
return repository.getFilms(pageable);
}
}

JPA repository with @EntityGraph

FilmJpaRepository now overrides findAll(Pageable) with an @EntityGraph that eagerly fetches the language relationships. Without it, accessing film.languageByLanguageId would trigger an N+1 query on every row in the page.

java/dev/pollito/spring_java/sakila/film/adapter/out/jpa/FilmJpaRepository.java
package dev.pollito.spring_java.sakila.film.adapter.out.jpa;

import dev.pollito.spring_java.sakila.generated.entity.Film;
import java.util.Optional;
import org.jspecify.annotations.NonNull;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FilmJpaRepository extends JpaRepository<Film, Integer> {

@EntityGraph(attributePaths = {"languageByLanguageId", "languageByOriginalLanguageId"})
Page<Film> findAll(@NonNull Pageable pageable);

@EntityGraph(attributePaths = {"languageByLanguageId", "languageByOriginalLanguageId"})
Optional<Film> findById(Integer id);
}

The @EntityGraph annotation tells Hibernate to fetch those relationships in the same query using a JOIN, instead of issuing a separate SELECT for each film's language.

Map Page<Entity> to Page<Film>

MapStruct (Java and Kotlin) and ModelMapper (Groovy) each handle the Page mapping differently.

In Java, a default method on the MapStruct interface delegates to source.map(this::map), which applies the existing single-entity mapper to every element and preserves pagination metadata:

java/dev/pollito/spring_java/sakila/film/adapter/out/jpa/FilmJpaMapper.java
package dev.pollito.spring_java.sakila.film.adapter.out.jpa;

import static org.mapstruct.MappingConstants.ComponentModel.SPRING;

import dev.pollito.spring_java.common.util.EnumUtils;
import dev.pollito.spring_java.sakila.film.domain.model.Film;
import dev.pollito.spring_java.sakila.film.domain.model.FilmLanguage;
import dev.pollito.spring_java.sakila.film.domain.model.FilmRating;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import org.jspecify.annotations.NonNull;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.springframework.data.domain.Page;

@Mapper(
componentModel = SPRING,
imports = {
EnumUtils.class,
FilmLanguage.class,
FilmRating.class,
OffsetDateTime.class,
ZoneOffset.class
})
public interface FilmJpaMapper {

@Mapping(target = "id", source = "filmId")
@Mapping(
target = "language",
expression =
"java(source.getLanguageByLanguageId() != null && source.getLanguageByLanguageId().getName() != null ? EnumUtils.fromValue(FilmLanguage.class, source.getLanguageByLanguageId().getName()) : null)")
@Mapping(
target = "originalLanguage",
expression =
"java(source.getLanguageByOriginalLanguageId() != null && source.getLanguageByOriginalLanguageId().getName() != null ? EnumUtils.fromValue(FilmLanguage.class, source.getLanguageByOriginalLanguageId().getName()) : null)")
@Mapping(
target = "releaseYear",
expression =
"java(source.getReleaseYear() != null ? source.getReleaseYear().getYear() : null)")
@Mapping(
target = "rating",
expression =
"java(source.getRating() != null ? EnumUtils.fromValue(FilmRating.class, source.getRating()) : null)")
@Mapping(
target = "lastUpdate",
expression =
"java(source.getLastUpdate() != null ? source.getLastUpdate().atOffset(ZoneOffset.UTC) : null)")
Film map(dev.pollito.spring_java.sakila.generated.entity.Film source);

default Page<Film> map(
@NonNull Page<dev.pollito.spring_java.sakila.generated.entity.Film> source) {
return source.map(this::map);
}
}

Repository implementation

FilmRepositoryImpl wires the JPA repository and mapper together:

java/dev/pollito/spring_java/sakila/film/adapter/out/jpa/FilmRepositoryImpl.java
package dev.pollito.spring_java.sakila.film.adapter.out.jpa;

import dev.pollito.spring_java.sakila.film.domain.model.Film;
import dev.pollito.spring_java.sakila.film.domain.port.out.FilmRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class FilmRepositoryImpl implements FilmRepository {
private final FilmJpaRepository repository;
private final FilmJpaMapper mapper;

@Override
public Film getFilm(Integer id) {
return mapper.map(repository.findById(id).orElseThrow());
}

@Override
public Page<Film> getFilms(Pageable pageable) {
return mapper.map(repository.findAll(pageable));
}
}

The getFilms(Pageable) method passes the Pageable straight to repository.findAll(...), gets back a Page<FilmEntity>, and converts it to Page<Film> via the mapper. The pagination metadata (total elements, total pages, current page info) travels along automatically because Page.map() preserves it.

Map the REST response

The OpenAPI spec defines FilmListResponse as an allOf combining ResponseMetadata (instance, status, timestamp, trace) with a data property. The data property itself is an allOf of the Spring Page schema and an object with a content array of Film items.

The REST-side mapper converts Page<Film> (domain) to FilmListResponseAllOfData (generated DTO):

MapStruct maps Page<Film> directly to FilmListResponseAllOfData, which mirrors the Spring Page structure (content, pageable, totalElements, totalPages):

java/dev/pollito/spring_java/sakila/film/adapter/in/rest/FilmRestMapper.java
package dev.pollito.spring_java.sakila.film.adapter.in.rest;

import static org.mapstruct.MappingConstants.ComponentModel.SPRING;

import dev.pollito.spring_java.sakila.film.domain.model.Film;
import dev.pollito.spring_java.sakila.generated.model.FilmListResponseAllOfData;
import org.mapstruct.Mapper;
import org.springframework.data.domain.Page;

@Mapper(componentModel = SPRING)
public interface FilmRestMapper {
dev.pollito.spring_java.sakila.generated.model.Film map(Film source);

FilmListResponseAllOfData map(Page<Film> source);
}

The controller method

The generated FilmsApi interface now declares getFilms(Pageable pageable) thanks to x-spring-paginated. The controller builds the response using the same pattern as the single-film endpoint:

java/dev/pollito/spring_java/sakila/film/adapter/in/rest/FilmRestController.java
package dev.pollito.spring_java.sakila.film.adapter.in.rest;

import static io.opentelemetry.api.trace.Span.current;
import static java.time.OffsetDateTime.now;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.http.ResponseEntity.ok;

import dev.pollito.spring_java.sakila.film.domain.port.in.FilmUseCases;
import dev.pollito.spring_java.sakila.generated.api.FilmsApi;
import dev.pollito.spring_java.sakila.generated.model.FilmFields;
import dev.pollito.spring_java.sakila.generated.model.FilmListResponse;
import dev.pollito.spring_java.sakila.generated.model.FilmResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class FilmRestController implements FilmsApi {
private final FilmUseCases useCases;
private final FilmRestMapper mapper;
private final HttpServletRequest request;

@Override
public ResponseEntity<FilmResponse> createFilm(FilmFields filmFields) {
throw new RuntimeException("Not implemented");
}

@Override
public ResponseEntity<Void> deleteFilm(Integer id) {
throw new RuntimeException("Not implemented");
}

@Override
public ResponseEntity<FilmResponse> getFilm(Integer id) {
return ok(
new FilmResponse()
.data(mapper.map(useCases.getFilm(id)))
.instance(request.getRequestURI())
.timestamp(now())
.trace(current().getSpanContext().getTraceId())
.status(OK.value()));
}

@Override
public ResponseEntity<FilmListResponse> getFilms(Pageable pageable) {
return ok(
new FilmListResponse()
.data(mapper.map(useCases.getFilms(pageable)))
.instance(request.getRequestURI())
.timestamp(now())
.trace(current().getSpanContext().getTraceId())
.status(OK.value()));
}

@Override
public ResponseEntity<FilmResponse> updateFilm(Integer id, FilmFields filmFields) {
throw new RuntimeException("Not implemented");
}
}

The Pageable parameter is resolved automatically by Spring MVC from the page, size, and sort query parameters, bounded by the defaults and max-page-size configured in application.yaml.

Test the pagination

Integration test — @DataJpaTest

The JPA repository test verifies that getFilms(PageRequest.of(0, 10)) returns a page with 10 films whose IDs start at 1:

java/dev/pollito/spring_java/sakila/film/adapter/out/jpa/FilmRepositoryImplDataJpaTest.java
package dev.pollito.spring_java.sakila.film.adapter.out.jpa;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.data.domain.PageRequest.of;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS;

import dev.pollito.spring_java.sakila.film.domain.model.Film;
import dev.pollito.spring_java.sakila.film.domain.port.out.FilmRepository;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

@DataJpaTest
@ActiveProfiles("test")
@Import({FilmRepositoryImpl.class, FilmJpaMapperImpl.class})
@Sql(
scripts = {"/sakila-schema.sql", "/sakila-data.sql"},
executionPhase = BEFORE_TEST_CLASS)
class FilmRepositoryImplDataJpaTest {

@SuppressWarnings("unused")
@Autowired
private FilmRepository repository;

@Test
void getFilmReturnsADomainModel() {
assertEquals(1, repository.getFilm(1).getId());
}

@Test
void getFilmsReturnsAPage() {
var page = repository.getFilms(of(0, 10));
assertNotNull(page);
assertEquals(10, page.getSize());
assertEquals(
List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
page.getContent().stream().map(Film::getId).toList());
}
}

Unit test — mocked repository

The use case test checks that getFilms delegates to the repository and returns the page:

java/dev/pollito/spring_java/sakila/film/domain/service/FilmUseCasesImplTest.java
package dev.pollito.spring_java.sakila.film.domain.service;

import static java.util.Collections.emptyList;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.data.domain.PageRequest.of;

import dev.pollito.spring_java.sakila.film.domain.model.Film;
import dev.pollito.spring_java.sakila.film.domain.port.out.FilmRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

@ExtendWith(MockitoExtension.class)
class FilmUseCasesImplTest {
@InjectMocks private FilmUseCasesImpl useCases;
@Mock private FilmRepository repository;

@Test
void getFilmReturnsADomainModel() {
when(repository.getFilm(anyInt())).thenReturn(mock(Film.class));
assertNotNull(useCases.getFilm(1));
}

@Test
void getFilmsReturnsAPage() {
when(repository.getFilms(any(Pageable.class)))
.thenReturn(new PageImpl<>(emptyList(), of(0, 10), 0));
assertNotNull(useCases.getFilms(of(0, 10)));
}
}

Reusable pagination assertions

The controller test needs to verify that the JSON response contains the pagination fields (content, pageable.pageNumber, pageable.pageSize, totalElements, totalPages). Instead of repeating those jsonPath assertions in every test, a shared hasPageFields() method is added to each language's MockMvcResultMatchers utility:

java/dev/pollito/spring_java/test/util/MockMvcResultMatchers.java
package dev.pollito.spring_java.test.util;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;

import org.springframework.http.HttpStatus;
import org.springframework.test.web.servlet.ResultMatcher;

public final class MockMvcResultMatchers {

private MockMvcResultMatchers() {}

public static ResultMatcher hasStandardApiResponseFields(
String expectedInstance, HttpStatus expectedStatus) {
return result -> {
jsonPath("$.instance").value(expectedInstance).match(result);
jsonPath("$.status").value(expectedStatus.value()).match(result);
jsonPath("$.timestamp").exists().match(result);
jsonPath("$.trace").exists().match(result);
};
}

public static ResultMatcher hasErrorFields(HttpStatus expectedStatus) {
return result -> jsonPath("$.title").value(expectedStatus.getReasonPhrase()).match(result);
}

public static ResultMatcher hasPageFields() {
return result -> {
jsonPath("$.data.content").isArray().match(result);
jsonPath("$.data.pageable.pageNumber").isNumber().match(result);
jsonPath("$.data.pageable.pageSize").isNumber().match(result);
jsonPath("$.data.totalElements").isNumber().match(result);
jsonPath("$.data.totalPages").isNumber().match(result);
};
}
}

hasPageFields() asserts that $.data.content is an array and that $.data.pageable.pageNumber, $.data.pageable.pageSize, $.data.totalElements, and $.data.totalPages are all numbers. The controller tests call it alongside hasStandardApiResponseFields().

Controller test — MockMvc

The controller test was previously asserting 500 INTERNAL_SERVER_ERROR (because getFilms threw RuntimeException("Not implemented")). Now it mocks useCases.getFilms(any(Pageable.class)) to return an empty page and asserts the response includes the standard API fields plus pagination fields (content, pageable.pageNumber, pageable.pageSize, totalElements, totalPages):

java/dev/pollito/spring_java/sakila/film/adapter/in/rest/FilmRestControllerMockMvcTest.java
package dev.pollito.spring_java.sakila.film.adapter.in.rest;

import static dev.pollito.spring_java.test.util.MockMvcResultMatchers.*;
import static java.util.Collections.emptyList;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.data.domain.PageRequest.of;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import dev.pollito.spring_java.config.web.ControllerAdvice;
import dev.pollito.spring_java.sakila.film.domain.model.Film;
import dev.pollito.spring_java.sakila.film.domain.port.in.FilmUseCases;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(FilmRestController.class)
@Import({ControllerAdvice.class, FilmRestMapperImpl.class})
class FilmRestControllerMockMvcTest {

public static final String PATH = "/api/films";
public static final String CONTENT_BODY =
"""
{
"title": "ACADEMY DINOSAUR",
"language": "English",
"rentalDuration": 3,
"rentalRate": 4.99,
"replacementCost": 20.99
}
""";

@SuppressWarnings("unused")
@Autowired
private MockMvc mockMvc;

@SuppressWarnings("unused")
@MockitoBean
private FilmUseCases filmUseCases;

@SuppressWarnings("unused")
@MockitoSpyBean
private FilmRestMapper mapper;

@Test
void getFilmReturnsOK() throws Exception {
Integer filmId = 1;
Film film = mock(Film.class);
when(film.getId()).thenReturn(filmId);

when(filmUseCases.getFilm(anyInt())).thenReturn(film);

mockMvc
.perform(get(PATH + "/{id}", filmId).accept(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(APPLICATION_JSON))
.andExpect(hasStandardApiResponseFields(PATH + "/" + filmId, OK));
}

@Test
void getFilmsReturnsOK() throws Exception {
when(filmUseCases.getFilms(any(Pageable.class)))
.thenReturn(new PageImpl<>(emptyList(), of(0, 10), 0));

mockMvc
.perform(get(PATH).accept(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(APPLICATION_JSON))
.andExpect(hasStandardApiResponseFields(PATH, OK))
.andExpect(hasPageFields());
}

@Test
void createFilmReturnsINTERNAL_SERVER_ERROR() throws Exception {
HttpStatus status = INTERNAL_SERVER_ERROR;
mockMvc
.perform(
post(PATH).contentType(APPLICATION_JSON).content(CONTENT_BODY).accept(APPLICATION_JSON))
.andExpect(hasStandardApiResponseFields(PATH, status))
.andExpect(hasErrorFields(status));
}

@Test
void deleteFilmReturnsINTERNAL_SERVER_ERROR() throws Exception {
Integer filmId = 1;
HttpStatus status = INTERNAL_SERVER_ERROR;
mockMvc
.perform(delete(PATH + "/{id}", filmId).accept(APPLICATION_JSON))
.andExpect(hasStandardApiResponseFields(PATH + "/" + filmId, status))
.andExpect(hasErrorFields(status));
}

@Test
void updateFilmReturnsINTERNAL_SERVER_ERROR() throws Exception {
Integer filmId = 1;
HttpStatus status = INTERNAL_SERVER_ERROR;
mockMvc
.perform(
put(PATH + "/{id}", filmId)
.contentType(APPLICATION_JSON)
.content(CONTENT_BODY)
.accept(APPLICATION_JSON))
.andExpect(hasStandardApiResponseFields(PATH + "/" + filmId, status))
.andExpect(hasErrorFields(status));
}
}

The complete flow

With pagination wired through every layer, a request to GET /api/films?page=0&size=5 flows like this:

Scroll to zoom • Drag corner to resize

Build and run the application with the dev profile, then hit the endpoint:

Terminal
curl -s "http://localhost:8080/api/films?page=0&size=5" | jq
{
"instance": "/api/films",
"status": 200,
"timestamp": "2026-04-02T18:30:00.123456789+01:00",
"trace": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"data": {
"content": [
{ "id": 1, "title": "ACADEMY DINOSAUR", ... },
{ "id": 2, "title": "ACE GOLDFINGER", ... },
{ "id": 3, "title": "ADAPTATION HOLES", ... },
{ "id": 4, "title": "AFFAIR PREJUDICE", ... },
{ "id": 5, "title": "AFRICAN EGG", ... }
],
"pageable": { "pageNumber": 0, "pageSize": 5 },
"totalElements": 1000,
"totalPages": 200
}
}