pagination skill
Configures various collection pagination strategies including page-based, cursor-based, and partial pagination within API Platform.
- GitHub stars
- 29
- Files
- 1
- Last commit
- Oct 22, 2026
- License
- —
- Allowed tools
- —
What does the pagination skill do?
pagination configures various collection pagination strategies including page-based, cursor-based, and partial pagination within API Platform.
Summary generated from the skill's SKILL.md.
About the pagination skill
Managing collection data sizes and response structures in API Platform is what this skill enables. Developers can configure how API resources handle large datasets by implementing various strategies such as page-based, cursor-based, or partial pagination. This is essential for preventing database performance issues caused by slow COUNT queries on massive tables or avoiding data drift during row insertions.
An agent should load this skill when a user needs to adjust items per page, enable client-controlled page sizes, or implement infinite scroll functionality. The skill provides guidance on using specific attributes like paginationPartial to skip total item calculations or paginationViaCursor to anchor results to unique fields. It also covers technical implementation details for custom state providers using ArrayPaginator or TraversablePaginator, ensuring that even non-standard data sources return proper Hydra views. Whether working with Doctrine ORM or Laravel Eloquent, this skill helps maintain availability and performance by setting limits like paginationMaximumItemsPerPage.
What an agent can do with it
- 1Prevent database DoS attacksAllow clients to choose their own page size while enforcing a strict maximum limit.
- 2Fix data drift issuesSwitch from page-based to cursor-based pagination to ensure stable results when rows are added or deleted.
Written from the skill's SKILL.md. Every use case is checked against a quote from it.
How to install the pagination skill
Each command copies the skill folder into the client's skills directory. Restart the client or start a new session to load it.Install the pagination skill in Plugin
/plugin marketplace add api-platform/skillset /plugin install api-platform@api-platform-skillset
Install the pagination skill in Claude Code
~/.claude/skills/pagination/SKILL.md
$ npx degit api-platform/skillset/skills/pagination ~/.claude/skills/paginationInstall the pagination skill in Claude Code (project)
.claude/skills/pagination/SKILL.md
$ npx degit api-platform/skillset/skills/pagination .claude/skills/paginationInstall the pagination skill in Codex
~/.agents/skills/pagination/SKILL.md
$ npx degit api-platform/skillset/skills/pagination ~/.agents/skills/paginationInstall the pagination skill in Gemini CLI
~/.gemini/skills/pagination/SKILL.md
$ npx degit api-platform/skillset/skills/pagination ~/.gemini/skills/paginationInstall the pagination skill in Claude.ai
npx degit api-platform/skillset/skills/pagination pagination && zip -r pagination.zip pagination # claude.ai → Customize → Skills → + → Create skill → Upload a skill → pagination.zip
SKILL.md
View on GitHubPagination
Pagination is enabled by default on every collection, 30 items per page, page
number in the page query parameter. The Hydra response carries a
PartialCollectionView with first/last/next/previous links and
totalItems. Most tuning is done with pagination* attributes on #[ApiResource]
or a single operation — operation-level wins over resource-level wins over global
config.
Items per page
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
#[ApiResource(paginationItemsPerPage: 20)] // resource default
#[GetCollection(paginationItemsPerPage: 100)] // override one operation
class Book {}
Global default:
# config/packages/api_platform.yaml
api_platform:
defaults:
pagination_items_per_page: 30
Letting the client choose the page size
Off by default (a client could otherwise ask for a million rows). Opt in, then cap
it with paginationMaximumItemsPerPage so the client can't DoS the database:
#[ApiResource(
paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 100,
)]
class Book {}
GET /books?itemsPerPage=50 now works (clamped to 100). The parameter name is
itemsPerPage by default; change it under collection.pagination.items_per_page_parameter_name.
Letting the client toggle pagination on/off
#[ApiResource(paginationClientEnabled: true)]
class Book {}
GET /books?pagination=false returns the full collection. The value goes through
PHP's FILTER_VALIDATE_BOOLEAN, so false/0/no all disable it.
Disabling pagination
Per resource or per operation:
#[ApiResource(paginationEnabled: false)]
#[GetCollection(paginationEnabled: false)]
class Book {}
Globally under defaults.pagination_enabled: false. Only do this for collections
you know stay small — an unbounded collection is an availability risk.
Partial pagination
Default pagination issues a COUNT query to compute totalItems and the last
page. On huge tables that COUNT dominates the request. Partial pagination skips it:
you lose totalItems and last, but keep a next link (computed by fetching one
extra row). This is the right mode for "infinite scroll" UIs that only need "is
there more?".
#[ApiResource(paginationPartial: true)]
#[GetCollection(paginationPartial: true)]
class Book {}
Client-controlled variant: paginationClientPartial: true enables
GET /books?partial=true.
Cursor-based pagination
Page-based pagination drifts when rows are inserted/deleted between requests (an
item can appear twice or be skipped). Cursor-based pagination anchors on a unique,
ordered field instead. It requires partial pagination plus a RangeFilter and an
OrderFilter on the cursor field:
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
#[ApiResource(
paginationPartial: true,
paginationViaCursor: [
['field' => 'id', 'direction' => 'DESC'],
],
)]
#[ApiFilter(RangeFilter::class, properties: ['id'])]
#[ApiFilter(OrderFilter::class, properties: ['id' => 'DESC'])]
class Book {}
The response's view then contains cursor links (id[lt]=…) instead of page=N.
Use the ODM filter namespace (ApiPlatform\Doctrine\Odm\Filter\…) for MongoDB.
Doctrine ORM paginator tuning
The ORM PaginationExtension inspects the QueryBuilder to decide two Doctrine
Paginator settings. Override the guesses when the query is unusual:
paginationFetchJoinCollection— settruewhen the query joins a collection-valued association (Doctrine then runs an extra query to count distinct roots correctly). API Platform usually detects this; force it when ato-manyjoin produces a wrongtotalItems.paginationUseOutputWalkers— output walkers are required for some queries (e.g.HAVING); they cost performance. Setfalseto disable when you know the query is simple,truewhen a complex query throws.
#[ApiResource(paginationFetchJoinCollection: false)]
#[GetCollection(name: 'with_join', paginationFetchJoinCollection: true)]
class Book {}
Pagination in custom state providers
The Doctrine/Eloquent providers paginate for you. A custom provider (see
state-provider) must return a paginator itself, or the collection comes back
unpaginated with no Hydra view. Return one of:
ApiPlatform\State\Pagination\ArrayPaginator— you already have all results in memory and want a page sliced out.ApiPlatform\State\Pagination\TraversablePaginator— you fetched exactly the current page (e.g. an external API that paginates server-side) and know the totals.
ArrayPaginator takes the full array plus offset/limit:
use ApiPlatform\State\Pagination\ArrayPaginator;
use ApiPlatform\State\Pagination\Pagination;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Metadata\Operation;
final class BookProvider implements ProviderInterface
{
public function __construct(private Pagination $pagination) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ArrayPaginator
{
$results = $this->fetchEverything();
[$page, $offset, $limit] = $this->pagination->getPagination($operation, $context);
return new ArrayPaginator($results, $offset, $limit);
}
}
TraversablePaginator is for when you only hold one page and know the totals — its
constructor is (\Traversable $items, float $currentPage, float $itemsPerPage, float $totalItems):
use ApiPlatform\State\Pagination\TraversablePaginator;
return new TraversablePaginator(
new \ArrayIterator($pageItems),
currentPage: $page,
itemsPerPage: $limit,
totalItems: $remoteTotalCount,
);
Inject ApiPlatform\State\Pagination\Pagination to resolve the effective page,
offset and limit from the operation + request — don't read ?page off the request
yourself, or you'll bypass the resource's paginationItemsPerPage/client toggles.
Both classes also implement HasNextPagePaginatorInterface (hasNextPage()), so a
partial-style next link works without a total. Implement
PartialPaginatorInterface directly only if you genuinely can't compute totals.
Laravel
The pagination* attributes (paginationItemsPerPage, paginationClientItemsPerPage,
paginationMaximumItemsPerPage, paginationEnabled, paginationPartial,
paginationClientEnabled/…Partial) and the Hydra view are the same. Differences:
- Global defaults live in
config/api-platform.phpunderdefaultsandpagination(pagination_items_per_page,page_parameter_name,items_per_page_parameter_name, etc.) — not YAML. - The Eloquent
CollectionProviderpaginates withpaginate()(full) orsimplePaginate()(partial), returningApiPlatform\Laravel\Eloquent\Paginator/PartialPaginator. Custom Eloquent providers can still return the framework-neutralArrayPaginator/TraversablePaginator. - Doctrine-only, no Eloquent equivalent:
paginationViaCursor(cursor pagination) and the ORM paginator tuning (paginationFetchJoinCollection,paginationUseOutputWalkers).
Checklist
- Default pagination left on for unbounded collections
-
paginationClientItemsPerPagepaired withpaginationMaximumItemsPerPage -
paginationPartialused where theCOUNTquery is the bottleneck - Cursor pagination has both
RangeFilterandOrderFilteron the cursor field - Custom providers return
ArrayPaginator/TraversablePaginator, not a bare array - Page/offset/limit read via injected
Pagination, not the raw request
Files in this skill
1pagination skill questions
How do I install the pagination skill in Claude Code?
Run npx degit api-platform/skillset/skills/pagination ~/.claude/skills/pagination. Claude Code loads skills from ~/.claude/skills (all projects) or .claude/skills (one project) and uses a skill when its description matches the task.
Which clients can use the pagination skill?
Any client that reads the Agent Skills format: Claude Code, Codex, Gemini CLI and claude.ai (upload as a ZIP). Scripts in the skill run only where the client allows code execution.
How can I prevent clients from requesting too many rows at once?
You can enable client-controlled page sizes using paginationClientItemsPerPage and then set a limit with paginationMaximumItemsPerPage to prevent database DoS attacks.
What is the benefit of using partial pagination?
Partial pagination skips the expensive COUNT query used to calculate total items, making it ideal for infinite scroll interfaces where you only need to know if a next page exists.
How do I implement cursor-based pagination?
Cursor-based pagination requires setting paginationPartial: true and defining paginationViaCursor with a unique ordered field, alongside a RangeFilter and OrderFilter on that field.
What should a custom state provider return to support pagination?
A custom provider must return an ArrayPaginator if you have all results in memory, or a TraversablePaginator if you have fetched a specific page and know the totals.