{"id":"mferpaa12dzdrag","title":"Spring Boot Content API Pagination for Headless Frontends","slug":"spring-boot-content-api-pagination","summary":"Offset-based paging is a performance trap because databases have to scan and discard every preceding row, which eventually causes timeouts as tables grow.…","imageUrl":"https://briancrabtree.me/images/journal-spring-boot-content-api-pagination.webp","category":"Backend","date":"2026-03-17T18:00:00.000Z","featured":false,"likes":30,"author":"Brian Crabtree","content":"<h2>Dealing with Growing Content Lists</h2>\n\n<p>My projects increasingly involve content APIs feeding headless frontends. When those content lists grow, fetching everything at once just doesn't scale. That's why implementing robust spring boot content api pagination is a core requirement, not an afterthought.</p>\n\n<p>A typical content feed might start small, but eventually, an editor adds hundreds or thousands of entries. Without proper pagination, your API either times out, consumes too much memory, or forces the client to download an unreasonable amount of data. None of those are good outcomes for user experience or server stability.</p>\n\n<p>The goal isn't just to split data; it's to provide a seamless, performant experience that doesn't break down as the data scales. This means thinking about database efficiency and client-side usability from the start, not as a quick fix later.</p>\n\n<pre><code>GET /api/posts?cursor=abc123&amp;limit=20\n{\n  \"items\": [...],\n  \"nextCursor\": \"def456\",\n  \"hasMore\": true\n}</code></pre>\n\n<p><figure>\n  <img src=\"/images/journal-inline-spring-pagination.webp\" alt=\"Cursor pagination flow diagram for Spring Boot content API\" width=\"1200\" height=\"675\" loading=\"lazy\" />\n  <figcaption>Cursor pagination scales — offset paging breaks on large tables.</figcaption>\n</figure></p>\n\n<h2>Offset-based Pagination Limits</h2>\n\n<p>The simplest approach, offset and limit, looks appealing initially. You just ask for 'page 2, 10 items per page.' Developers usually grab `Pageable` in Spring Data JPA and call it a day, passing `page=1&size=10` to the controller.</p>\n\n<p>This breaks down fast under load. If items are added or removed from the beginning of the list while a user is paginating, the dataset shifts. A user might skip items or see duplicates. Databases also struggle with large offsets, scanning many rows just to discard them before reaching the desired window.</p>\n\n<p>I've seen production systems crawl to a halt because a simple content feed hit thousands of entries, and every subsequent page required the database to re-scan a progressively larger chunk of the table. It's a performance bomb waiting to detonate, especially when sorting by non-indexed columns.</p>\n\n<h2>Cursor-based for Reliability</h2>\n\n<p>This is why I push for cursor-based pagination whenever feasible. Instead of an integer offset, the client sends a reference point – a 'cursor' – which is usually the ID or a timestamp of the last item received. The backend then fetches items *after* that cursor.</p>\n\n<p>This approach is far more stable. Data additions or deletions only affect future pages, not the current or previous set, preventing the 'missing item' problem. Performance is better too, as the database uses an index to jump straight to the cursor's location, avoiding full table scans.</p>\n\n<p>It means more state on the client, specifically the `next` or `prev` cursor, but that's a small trade-off for significant backend stability and speed. It moves the complexity to where it's often easier to manage: the client-side state machine for fetching data.</p>\n\n<h2>Spring Data's Paging and Sorting</h2>\n\n<p>Spring Data JPA makes handling basic pagination much cleaner. Your repository methods can take a `Pageable` object directly, and they return a `Page<T>` or `Slice<T>`. This abstraction handles much of the boilerplate SQL generation.</p>\n\n<p>The `Pageable` interface includes page number, size, and sort information. `Page<T>` then gives you not just the content, but also total elements, total pages, and whether it's the first or last page. For a simple content API using page numbers, this is usually enough to start.</p>\n\n<p>When moving to cursor-based, you'll still leverage `Sort.by()` within your `Pageable` or build custom queries. The key is that the cursor value itself dictates the `WHERE` clause, typically `WHERE id > :lastId ORDER BY id ASC LIMIT :pageSize` for forward pagination. You pass the `lastId` as part of the cursor.</p>\n\n<h2>Frontend Integration and REST Contracts</h2>\n\n<p>For a `rest pagination frontend`, the JSON response needs to expose the necessary pagination metadata. That includes the actual content, but also properties like `currentPage`, `pageSize`, `totalElements`, and crucially, the `nextCursor` and `prevCursor`.</p>\n\n<p>I usually map these directly into the API response as clear fields, rather than relying heavily on HATEOAS `_links` for primary navigation in simple content feeds. For many frontend developers, direct `nextPageToken` or `lastItemId` fields are clearer than parsing a complex link rel structure.</p>\n\n<p>The frontend then takes this cursor, stores it, and sends it back with the next request. This design is robust and clearly defines the contract between the backend and any `headless cms spring boot` driven client, minimizing ambiguity and errors.</p>\n\n<h2>Headless CMS Demands</h2>\n\n<p>When dealing with a headless CMS, the frontend consuming the API is often a complex, single-page application. They need flexible pagination without much server-side interpretation. The backend should provide all necessary context upfront, not just the raw data.</p>\n\n<p>This means ensuring that the pagination mechanism remains consistent regardless of filters or sorting applied. A user filtering by category should still get reliable pagination, ideally using a stable cursor for that specific filter set. Consistency here is critical; it prevents tricky bugs on the client. For more on managing API structure, check out my thoughts on robust API versioning at <a href=\"/journal/backend-json-api-design-for-frontends/\">Backend JSON APIs: Shapes I Design So Frontends Stay Fast</a>.</p>\n\n<p>I also consider edge cases: what happens when no content matches a filter? The API should return an empty content array with appropriate `totalElements: 0` and no cursors, rather than an error. Clear, predictable contracts prevent unnecessary headaches on the frontend.</p>\n\n<h2>What I do next</h2>\n\n<p>Going forward, I'm doubling down on explicit cursor-based pagination for high-volume content feeds. It's more work upfront than a simple offset, but it pays dividends in stability, performance, and a cleaner developer experience for frontend teams. The initial setup cost is quickly recouped.</p>\n\n<p>The initial investment into a proper `Pageable` abstraction in Spring, coupled with custom query methods for cursor logic, streamlines future content additions and modifications. It standardizes how content APIs behave, which is critical for scalable systems that need to evolve without breaking existing clients.</p>\n\n<p>If you're tackling similar challenges with Java and Spring, especially in a headless setup, check out my more in-depth guide on building Spring content APIs for headless at <a href=\"/journal/java-spring-content-apis-headless/\">Java and Spring Content APIs for Headless Frontends</a>. It walks through the code and explains the choices in detail.</p>","tags":["spring-boot","pagination","headless"],"views":73}