Como Implementar Paginação em APIs REST: Guia Passo a Passo

Mark Ponomarev

Mark Ponomarev

19 maio 2025

Como Implementar Paginação em APIs REST: Guia Passo a Passo

Ao construir APIs REST que retornam uma lista de recursos, é crucial considerar como lidar com grandes conjuntos de dados. Retornar milhares ou até milhões de registros em uma única resposta de API é impraticável e pode levar a problemas significativos de desempenho, alto consumo de memória tanto para o servidor quanto para o cliente, e uma experiência de usuário ruim. A paginação é a solução padrão para este problema. Ela envolve a divisão de um grande conjunto de dados em partes menores e gerenciáveis, chamadas "páginas", que são então servidas sequencialmente. Este tutorial irá guiá-lo pelos passos técnicos da implementação de várias estratégias de paginação em suas APIs REST.

💡
Quer uma ótima ferramenta de Teste de API que gera documentação de API bonita?

Quer uma plataforma integrada e All-in-One para sua Equipe de Desenvolvedores trabalharem juntos com máxima produtividade?

Apidog entrega todas as suas demandas, e substitui o Postman por um preço muito mais acessível!
button

Por que a Paginação é Essencial?

Antes de mergulhar nos detalhes de implementação, vamos abordar brevemente por que a paginação é um recurso inegociável para APIs que lidam com coleções de recursos:

  1. Desempenho: Solicitar e transferir grandes quantidades de dados pode ser lento. A paginação reduz o tamanho do payload de cada requisição, levando a tempos de resposta mais rápidos e menor carga no servidor.
  2. Consumo de Recursos: Respostas menores consomem menos memória no servidor que as gera e no cliente que as processa. Isso é especialmente crítico para clientes móveis ou ambientes com recursos limitados.
  3. Limite de Taxa (Rate Limiting) e Cotas: Muitas APIs impõem limites de taxa. A paginação ajuda os clientes a permanecerem dentro desses limites buscando dados em partes menores ao longo do tempo, em vez de tentar obter tudo de uma vez.
  4. Experiência do Usuário: Para interfaces de usuário que consomem a API, apresentar dados em páginas é muito mais amigável do que sobrecarregar os usuários com uma lista enorme ou uma rolagem muito longa.
  5. Eficiência do Banco de Dados: Buscar um subconjunto de dados é geralmente menos custoso para o banco de dados em comparação com a recuperação de uma tabela inteira, especialmente se a indexação adequada estiver em vigor.

Estratégias Comuns de Paginação

Existem várias estratégias comuns para implementar paginação, cada uma com seus próprios trade-offs. Exploraremos as mais populares: offset/limit (frequentemente referida como paginação baseada em página) e baseada em cursor (também conhecida como keyset ou seek pagination).

1. Paginação Offset/Limit (ou Baseada em Página)

Esta é, sem dúvida, o método de paginação mais direto e amplamente adotado. Funciona permitindo que o cliente especifique dois parâmetros principais:

Alternativamente, os clientes podem especificar:

O offset pode ser calculado a partir de page e pageSize usando a fórmula: offset = (page - 1) * pageSize.

Passos Técnicos de Implementação:

Vamos supor que temos um endpoint de API /items que retorna uma lista de itens.

a. Parâmetros da Requisição API:
O cliente faria uma requisição como:
GET /items?offset=20&limit=10 (buscar 10 itens, pulando os primeiros 20)
ou
GET /items?page=3&pageSize=10 (buscar a 3ª página, com 10 itens por página, o que é equivalente a offset=20, limit=10).

É uma boa prática definir valores padrão para esses parâmetros (por exemplo, limit=20, offset=0 ou page=1, pageSize=20) se o cliente não os fornecer. Além disso, imponha um limit ou pageSize máximo para evitar que os clientes solicitem um número excessivamente grande de registros, o que poderia sobrecarregar o servidor.

b. Lógica de Backend (Conceitual):
Quando o servidor recebe esta requisição, ele precisa traduzir esses parâmetros em uma consulta ao banco de dados.

// Exemplo em Java com Spring Boot
@GetMapping("/items")
public ResponseEntity<PaginatedResponse<Item>> getItems(
    @RequestParam(defaultValue = "0") int offset,
    @RequestParam(defaultValue = "20") int limit
) {
    // Validar limite para prevenir abuso
    if (limit > 100) {
        limit = 100; // Impor um limite máximo
    }

    List<Item> items = itemRepository.findItemsWithOffsetLimit(offset, limit);
    long totalItems = itemRepository.countTotalItems(); // Para metadados

    // Construir e retornar resposta paginada
    // ...
}

c. Consulta ao Banco de Dados (Exemplo SQL):
A maioria dos bancos de dados relacionais suporta cláusulas offset e limit diretamente.

Para PostgreSQL ou MySQL:

SELECT *
FROM items
ORDER BY created_at DESC -- Ordenação consistente é crucial para paginação estável
LIMIT 10 -- Este é o parâmetro 'limit'
OFFSET 20; -- Este é o parâmetro 'offset'

Para SQL Server (versões mais antigas podem usar ROW_NUMBER()):

SELECT *
FROM items
ORDER BY created_at DESC
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;

Para Oracle:

SELECT *
FROM (
    SELECT i.*, ROWNUM rnum
    FROM (
        SELECT *
        FROM items
        ORDER BY created_at DESC
    ) i
    WHERE ROWNUM <= 20 + 10 -- offset + limit
)
WHERE rnum > 20; -- offset

Nota Importante sobre Ordenação: Para que a paginação offset/limit seja confiável, o conjunto de dados subjacente deve ser ordenado por uma chave consistente e única (ou quase única), ou uma combinação de chaves. Se a ordem dos itens puder mudar entre as requisições (por exemplo, novos itens sendo inseridos ou itens sendo atualizados de forma que afete sua ordem de classificação), os usuários podem ver itens duplicados ou perder itens ao navegar pelas páginas. Uma escolha comum é ordenar por timestamp de criação ou por um ID primário.

d. Estrutura da Resposta da API:
Uma boa resposta paginada deve incluir não apenas os dados da página atual, mas também metadados para ajudar o cliente a navegar.

{
  "data": [
    // array de itens para a página atual
    { "id": "item_21", "name": "Item 21", ... },
    { "id": "item_22", "name": "Item 22", ... },
    // ... até 'limit' itens
    { "id": "item_30", "name": "Item 30", ... }
  ],
  "pagination": {
    "offset": 20,
    "limit": 10,
    "totalItems": 5000, // Número total de itens disponíveis
    "totalPages": 500, // Calculado como ceil(totalItems / limit)
    "currentPage": 3 // Calculado como (offset / limit) + 1
  },
  "links": { // Links HATEOAS para navegação
    "self": "/items?offset=20&limit=10",
    "first": "/items?offset=0&limit=10",
    "prev": "/items?offset=10&limit=10", // Nulo se estiver na primeira página
    "next": "/items?offset=30&limit=10", // Nulo se estiver na última página
    "last": "/items?offset=4990&limit=10"
  }
}

Fornecer links HATEOAS (Hypermedia as the Engine of Application State) (self, first, prev, next, last) é uma boa prática REST. Isso permite que os clientes naveguem pelas páginas sem ter que construir as URLs eles mesmos.

Prós da Paginação Offset/Limit:

Contras da Paginação Offset/Limit:

2. Paginação Baseada em Cursor (Keyset/Seek)

A paginação baseada em cursor aborda algumas das deficiências do offset/limit, particularmente o desempenho com grandes conjuntos de dados e problemas de consistência de dados. Em vez de depender de um offset absoluto, ela usa um "cursor" que aponta para um item específico no conjunto de dados. O cliente então solicita itens "depois" ou "antes" deste cursor.

O cursor é tipicamente uma string opaca que codifica o(s) valor(es) da(s) chave(s) de ordenação do último item recuperado na página anterior.

Passos Técnicos de Implementação:

a. Parâmetros da Requisição API:
O cliente faria uma requisição como:
GET /items?limit=10 (para a primeira página)
E para páginas subsequentes:
GET /items?limit=10&after_cursor=stringopaquarepresentandoidoultimoitem
Ou, para paginar para trás (menos comum, mas possível):
GET /items?limit=10&before_cursor=stringopaquarepresentandoidprimeiroitem

O parâmetro limit ainda define o tamanho da página.

b. O que é um Cursor?
Um cursor deve ser:

c. Lógica de Backend (Conceitual):

// Exemplo em Java com Spring Boot
@GetMapping("/items")
public ResponseEntity<CursorPaginatedResponse<Item>> getItems(
    @RequestParam(defaultValue = "20") int limit,
    @RequestParam(required = false) String afterCursor
) {
    // Validar limite
    if (limit > 100) {
        limit = 100;
    }

    // Decodificar cursor para obter as propriedades do último item visto
    // e.g., LastSeenItemDetails lastSeen = decodeCursor(afterCursor);
    // Se afterCursor for nulo, é a primeira página.

    List<Item> items;
    if (afterCursor != null) {
        DecodedCursor decoded = decodeCursor(afterCursor); // e.g., { lastId: "some_uuid", lastCreatedAt: "timestamp" }
        items = itemRepository.findItemsAfter(decoded.getLastCreatedAt(), decoded.getLastId(), limit);
    } else {
        items = itemRepository.findFirstPage(limit);
    }

    String nextCursor = null;
    if (!items.isEmpty() && items.size() == limit) {
        // Assumindo que os itens estão ordenados, o último item da lista é usado para gerar o próximo cursor
        Item lastItemOnPage = items.get(items.size() - 1);
        nextCursor = encodeCursor(lastItemOnPage.getCreatedAt(), lastItemOnPage.getId());
    }

    // Construir e retornar resposta paginada baseada em cursor
    // ...
}

// Métodos auxiliares para codificar/decodificar cursores
// private DecodedCursor decodeCursor(String cursor) { ... }
// private String encodeCursor(Timestamp createdAt, String id) { ... }

d. Consulta ao Banco de Dados (Exemplo SQL):
A chave é usar uma cláusula WHERE que filtra registros com base na(s) chave(s) de ordenação do cursor. A cláusula ORDER BY deve estar alinhada com a composição do cursor.

Assumindo ordenação por created_at (descendente) e depois por id (descendente) como desempate para ordenação estável se created_at não for único:

Para a primeira página:

SELECT *
FROM items
ORDER BY created_at DESC, id DESC
LIMIT 10;

Para páginas subsequentes, se o cursor decodificado for last_created_at_from_cursor e last_id_from_cursor:

SELECT *
FROM items
WHERE (created_at, id) < (CAST('last_created_at_from_cursor' AS TIMESTAMP), CAST('last_id_from_cursor' AS UUID)) -- Ou tipos apropriados
-- Para ordem ascendente, seria >
-- A comparação de tupla (created_at, id) < (val1, val2) é uma forma concisa de escrever:
-- WHERE created_at < 'last_created_at_from_cursor'
--    OR (created_at = 'last_created_at_from_cursor' AND id < 'last_id_from_cursor')
ORDER BY created_at DESC, id DESC
LIMIT 10;

Este tipo de consulta é muito eficiente, especialmente se houver um índice em (created_at, id). O banco de dados pode "buscar" diretamente o ponto de partida sem escanear linhas irrelevantes.

e. Estrutura da Resposta da API:

{
  "data": [
    // array de itens para a página atual
    { "id": "item_N", "createdAt": "2023-10-27T10:05:00Z", ... },
    // ... até 'limit' itens
    { "id": "item_M", "createdAt": "2023-10-27T10:00:00Z", ... }
  ],
  "pagination": {
    "limit": 10,
    "hasNextPage": true, // booleano indicando se há mais dados
    "nextCursor": "stringopaquacodificadaembase64paraitem_M" // string opaca
    // Potencialmente um "prevCursor" se cursores bidirecionais forem suportados
  },
  "links": {
    "self": "/items?limit=10&after_cursor=cursor_da_requisicao_atual_se_houver",
    "next": "/items?limit=10&after_cursor=stringopaquacodificadaembase64paraitem_M" // Nulo se não houver próxima página
  }
}

Observe que a paginação baseada em cursor tipicamente não fornece totalPages ou totalItems porque calcular estes exigiria um scan completo da tabela, negando alguns dos benefícios de desempenho. Se estes forem estritamente necessários, um endpoint separado ou uma estimativa podem ser fornecidos.

Prós da Paginação Baseada em Cursor:

Contras da Paginação Baseada em Cursor:

Escolhendo a Estratégia Certa

A escolha entre paginação offset/limit e baseada em cursor depende dos seus requisitos específicos:

Em alguns sistemas, uma abordagem híbrida é até usada, ou diferentes estratégias são oferecidas para diferentes casos de uso ou endpoints.

Melhores Práticas para Implementar Paginação

Independentemente da estratégia escolhida, adira a estas melhores práticas:

  1. Nomenclatura Consistente de Parâmetros: Use nomes claros e consistentes para seus parâmetros de paginação (por exemplo, limit, offset, page, pageSize, after_cursor, before_cursor). Mantenha uma convenção (por exemplo, camelCase ou snake_case) em toda a sua API.
  2. Forneça Links de Navegação (HATEOAS): Conforme mostrado nos exemplos de resposta, inclua links para self, next, prev, first e last (onde aplicável). Isso torna a API mais descobrível e desacopla o cliente da lógica de construção de URL.
  3. Valores Padrão e Limites Máximos:
  1. Documentação Clara da API: Documente sua estratégia de paginação completamente:
  1. Ordenação Consistente: Garanta que os dados subjacentes sejam ordenados consistentemente para cada requisição paginada. Para offset/limit, isso é vital para evitar desvio de dados. Para baseada em cursor, a ordem de classificação dita como os cursores são construídos e interpretados. Use uma coluna de desempate única (como um ID primário) se a coluna de classificação primária puder ter valores duplicados.
  2. Lide com Casos Limite:
  1. Considerações sobre Contagem Total:
  1. Tratamento de Erros: Retorne códigos de status HTTP apropriados para erros (por exemplo, 400 para entrada inválida, 500 para erros do servidor durante a busca de dados).
  2. Segurança: Embora não seja diretamente um mecanismo de paginação, garanta que os dados sendo paginados respeitem as regras de autorização. Um usuário só deve poder paginar dados que ele tem permissão para ver.
  3. Cache: Respostas paginadas frequentemente podem ser cacheadas. Para paginação baseada em offset, GET /items?page=2&pageSize=10 é altamente cacheável. Para baseada em cursor, GET /items?limit=10&after_cursor=XYZ também é cacheável. Garanta que sua estratégia de cache funcione bem com a forma como os links de paginação são gerados e consumidos. Estratégias de invalidação precisam ser consideradas se os dados subjacentes mudarem frequentemente.

Tópicos Avançados (Menções Breves)

Conclusão

Implementar a paginação corretamente é fundamental para construir APIs REST escaláveis e amigáveis ao usuário. Embora a paginação offset/limit seja mais simples para começar, a paginação baseada em cursor oferece desempenho e consistência superiores para conjuntos de dados grandes e dinâmicos. Ao entender os detalhes técnicos de cada estratégia, escolher aquela que melhor se adapta às necessidades da sua aplicação e seguir as melhores práticas de implementação e design de API, você pode garantir que sua API entregue dados eficientemente aos seus clientes, independentemente da escala. Lembre-se de sempre priorizar documentação clara e tratamento robusto de erros para fornecer uma experiência suave aos consumidores da API.


Pratique o design de API no Apidog

Descubra uma forma mais fácil de construir e usar APIs