Qual estrategia de paginacao sua API realmente precisa? Offset parece a resposta obvia, mas pode nao ser. A escolha errada afeta performance, consistencia e a experiencia de quem consome seus dados.
Este guia mostra como implementar paginacao em API do zero, com criterios objetivos para cada decisao. Voce vai sair com um passo a passo aplicavel, os erros mais comuns e um checklist final para revisar antes de publicar.
Passo 1: Defina o cenario antes de escolher a estrategia
A primeira decisao nao e tecnica, e de negocio. Pergunte: seus dados mudam com frequencia? O volume vai crescer muito? Quem consome a API quer navegar por paginas especificas ou apenas percorrer tudo em sequencia?
Dados estaveis, como um catalogo de produtos que muda pouco, funcionam bem com offset. Dados volateis, como feeds de eventos ou logs, exigem cursor, porque novas entradas deslocam o offset e causam itens repetidos ou pulados.
Um criterio mensuravel: se sua tabela tem mais de 100 mil registros e recebe insercao constante, offset tende a degradar. A consulta com OFFSET alto obriga o banco a ler e descartar linhas anteriores, e o custo cresce a cada pagina.
Passo 2: Implemente paginacao por offset com limites claros
Offset usa dois parametros: page (numero da pagina) ou offset (quantos registros pular) e limit (quantos retornar). O exemplo mais comum em APIs REST:
GET /api/produtos?offset=40&limit=20
Isso retorna 20 registros a partir do 41º. A resposta deve incluir o total de itens para que o cliente calcule o numero de paginas.
{ "data": [...], "pagination": { "offset": 40, "limit": 20, "total": 253 } }
Erro comum: aceitar qualquer valor de limit. Defina um teto, por exemplo 100, e um padrao, como 20. Sem limite, um cliente pode pedir 1 milhao de registros e derrubar o servidor.
Dica: valide se offset nao e negativo e se limit nao ultrapassa o teto. Responda 400 com mensagem clara, em vez de processar um pedido invalido.
Passo 3: Use cursor para dados em constante mudanca
Cursor pagination usa um ponteiro opaco, geralmente um ID ou timestamp codificado, que indica a posicao exata da ultima leitura.
GET /api/eventos?cursor=eyJpZCI6MTIzNDV9&limit=20
O servidor decodifica o cursor e monta a consulta com condicao WHERE id > cursor_id ORDER BY id ASC. Nao ha salto de registros quando novos itens chegam, pois o cursor aponta para um lugar fixo.
A resposta inclui o proximo cursor, que pode ser nulo quando nao ha mais paginas:
{ "data": [...], "pagination": { "next_cursor": "eyJpZCI6MTIzNjV9", "has_more": true } }
Erro comum: usar cursor baseado em dados que mudam, como um campo editavel. Se o valor que ordena o cursor for alterado, a consulta pode pular ou repetir itens. Use ID sequencial ou timestamp de criacao, que sao imutaveis.
Dica: nao deixe o cliente decodificar o cursor. Ele deve ser opaco, tratado como uma string qualquer. Se o formato vazar, alguem pode manipular e obter dados fora do escopo.
Passo 4: Adote links de navegacao no padrao RFC 5988
Em vez de obrigar o cliente a montar URLs, retorne links prontos no header Link da resposta HTTP. O padrao e reconhecido por clientes HTTP e bibliotecas.
Link: <https://api.exemplo.com/produtos?offset=40&limit=20>; rel="next", <https://api.exemplo.com/produtos?offset=0&limit=20>; rel="first"
Os rels mais comuns sao first, last, next e prev. Quando nao houver proxima pagina, omita o link next, em vez de enviar vazio.
Isso simplifica o cliente: ele so precisa seguir o link next ate que ele suma. Nao depende de calcular offset nem de conhecer o total.
Erro comum: incluir last quando o total e desconhecido. Se voce usa cursor, nao tem como saber a ultima pagina sem percorrer tudo. Nesse caso, omita last e use apenas next.
Dica: combine os links com a resposta JSON. Coloque os mesmos links no corpo, em um campo links, para clientes que preferem nao ler headers.
Passo 5: Defina a ordem dos dados de forma estavel
Paginacao sem ordenacao definida e aleatoria. O banco pode retornar linhas em ordem diferente entre requisicoes, causando itens duplicados ou ausentes.
Sempre inclua uma clausula ORDER BY com coluna unica ou composta. O ideal e usar a mesma coluna do cursor, como ID ou data de criacao.
SELECT * FROM produtos ORDER BY id ASC LIMIT 20 OFFSET 40;
Se precisar ordenar por outro campo, como nome, adicione o ID como desempate:
ORDER BY nome ASC, id ASC
Isso garante que registros com o mesmo nome tenham posicao fixa. Sem o desempate, a ordem entre eles pode variar e quebrar a paginacao.
Erro comum: aplicar paginacao sobre uma view ou consulta com JOIN caro sem indexar a coluna de ordenacao. O banco faz um sort completo a cada requisicao, e o tempo de resposta cresce de forma descontrolada.
Dica: crie um indice composto na coluna de ordenacao e na de filtro. Por exemplo, (categoria_id, id) para uma consulta que filtra por categoria e pagina por ID.
Passo 6: Documente o comportamento para quem consome
Documentacao de paginacao nao e luxo, e parte da implementacao. Sem ela, cada cliente vai interpretar os parametros do seu jeito.
Especifique no minimo: nomes dos parametros (offset, limit, cursor), valores padrao e maximos, formato da resposta e o que acontece quando o offset ultrapassa o total.
Um caso comum: cliente pede offset=9999 quando existem 100 registros. Decida se retorna lista vazia com 200 ou erro 404. O comportamento precisa ser documentado e consistente.
Erro comum: mudar o formato de paginacao entre versoes sem aviso. Se voce trocar de offset para cursor, versione a API ou mantenha os dois por um periodo de transicao.
Dica: use exemplos reais na documentacao, com URLs completas e respostas JSON. Isso reduz duvidas e suporte.
Passo 7: Teste com volume e casos de borda
Testar paginacao so com 10 registros nao prova nada. O problema aparece quando a tabela tem milhoes de linhas ou quando a API recebe requisicoes concorrentes.
Crie testes para: primeira pagina, ultima pagina, offset no limite, limit acima do maximo, cursor invalido e dados inseridos entre requisicoes.
Um teste pratico: insira um registro entre a primeira e a segunda requisicao e verifique se ele aparece na pagina correta. Com offset, ele desloca os demais. Com cursor, ele aparece na proxima leitura, sem pular nenhum item.
Erro comum: testar apenas com o banco local e esquecer o ambiente de producao, onde o volume e a latencia sao diferentes. O tempo de resposta de um OFFSET 100000 em producao pode ser inviavel.
Dica: monitore o tempo de resposta por pagina. Se ele cresce conforme o offset aumenta, sua estrategia nao escala. Considere migrar para cursor.
Checklist final: o que revisar antes de publicar
Percorra esta lista antes de considerar a paginacao pronta:
- Estrategia escolhida conforme o cenario: offset para dados estaveis, cursor para dados volateis.
- Parametros com valores padrao e limite maximo definidos.
- Validacao de parametros invalidos com resposta 400 clara.
- Ordenacao estavel com coluna unica ou composta.
- Indice criado na coluna de ordenacao e filtro.
- Links de navegacao (RFC 5988) ou campos de cursor na resposta.
- Comportamento de offset alem do total documentado.
- Testes com volume e casos de borda executados.
- Tempo de resposta monitorado e aceitavel.
FAQ: perguntas frequentes sobre paginacao em API
Offset ou cursor: qual devo usar?
Offset e mais simples e funciona bem para dados estaveis e volumes moderados, ate dezenas de milhares de registros. Cursor e obrigatorio para grandes volumes ou dados que mudam com frequencia, pois evita saltos e repeticoes. Avalie o crescimento dos seus dados antes de decidir.
Como evitar itens duplicados na paginacao?
Use ordenacao estavel com coluna unica ou composta, como ORDER BY id ASC. Se usar offset e novos registros forem inseridos, itens podem duplicar. Cursor resolve esse problema ao fixar a posicao da ultima leitura.
Qual o limite ideal para o parametro limit?
Nao existe numero universal. O padrao comum e 20 ou 50, com teto entre 100 e 200. O limite depende do tamanho de cada registro e da capacidade do servidor. Comece conservador e aumente com base em metricas de performance.
Posso usar paginacao baseada em page e page_size?
Pode, mas page e page_size sao uma variacao do offset. O servidor converte para OFFSET = (page - 1) * page_size. O problema de performance com offsets altos continua. Para consistencia, prefira offset e limit ou cursor.
Como retornar o total de paginas na resposta?
Com offset, inclua o campo total e o cliente calcula o numero de paginas. Com cursor, nao ha como saber o total sem percorrer tudo. Nesse caso, use has_more para indicar se existem mais registros.
O que fazer quando o offset ultrapassa o total de registros?
Decida um comportamento consistente e documente. A pratica mais comum e retornar 200 com lista vazia e o total correto. Algumas APIs retornam 404, mas isso confunde clientes que percorrem paginas ate o fim. Escolha uma e mantenha.
A implementacao de paginacao parece detalhe, mas define a experiencia de quem usa sua API. Offset resolve o basico, cursor resolve o que escala. O que falta provar no seu caso e qual estrategia aguenta o volume real de producao, nao o de um ambiente de teste com 50 registros.