Paginação, filtros e ordenação
Paginação por deslocamento ou por cursor, e por que a primeira degrada com tabelas grandes e repete itens quando chegam dados novos.
As decisões de uma rota de listagem são cinco: a forma de paginar, o formato do cursor, os limites, como filtrar e ordenar, e o que fazer com o total.
Deslocamento ou cursor
Deslocamento (OFFSET) | Cursor | |
|---|---|---|
| Custo da página | Cresce com a profundidade (214 ms no deslocamento 1 milhão, no teste) | Constante (0,09 ms no mesmo ponto) |
| Itens novos entre páginas | Repete ou pula itens | Não repete nem pula |
| Pular para a página 37 | Sim | Não |
| "Página 3 de 120" | Sim, com uma contagem | Não diretamente |
| Ordenação | Qualquer uma | Precisa de um índice que cubra a ordenação, com desempate único |
| Implementação | Trivial | Um pouco mais elaborada |
Quando cada uma faz sentido:
| Situação | Escolha |
|---|---|
| APIs públicas, integrações, sincronização de dados | Cursor |
| Feeds, mensagens, eventos, listas que crescem sem parar | Cursor |
| Rolagem infinita em aplicativos | Cursor |
| Telas administrativas com números de página, sobre tabelas pequenas | Deslocamento |
| Relatórios com saltos para páginas específicas | Deslocamento, com limite de profundidade |
Uma combinação comum: cursor como padrão da API, e deslocamento apenas em telas internas, com um limite de profundidade (por exemplo, no máximo 10 mil itens), além do qual a tela pede para refinar a busca.
O formato do cursor
| Decisão | Recomendação |
|---|---|
| Opaco | Codificar em base64 os valores de ordenação. O cliente devolve sem interpretar, e o servidor pode mudar o formato interno sem quebrar ninguém |
| Precisão | Os valores com a mesma precisão com que o banco compara: horários com microssegundos, números grandes como texto |
| Validação | Um cursor malformado ou adulterado responde 400, e nunca vai direto para a consulta |
| Assinatura, se necessário | Assinar o cursor com uma chave do servidor, quando os valores dentro dele não devem ser alterados pelo cliente |
| Filtros | Um cursor vale para os mesmos filtros e a mesma ordenação; incluir ambos no cursor, ou recusar a mudança, evita páginas inconsistentes |
Limites
| Regra | Por quê |
|---|---|
| Tamanho padrão (ex.: 20) | O cliente que não informa nada recebe uma página razoável |
| Tamanho máximo (ex.: 100) | ?limite=1000000 não pode virar uma consulta de um milhão de linhas |
| Buscar um item a mais do que o pedido | Saber se existe próxima página sem fazer outra consulta |
Filtros e ordenação
GET /pedidos?status=pago&clienteId=42&criadoDesde=2026-03-01&ordem=-criadoEm&limite=20| Decisão | Recomendação |
|---|---|
| Nomes dos filtros | Os mesmos nomes dos campos da resposta |
| Intervalos | Sufixos claros: criadoDesde, criadoAte; ou totalMin, totalMax |
| Ordenação | Um parâmetro ordem com o campo e o sinal para decrescente (-criadoEm) |
| Campos permitidos | Uma lista fechada de campos filtráveis e ordenáveis, cada um com índice que o atenda |
| Valores inválidos | 400 com o parâmetro e o motivo, no formato de erro da API |
A lista fechada é importante por dois motivos. Primeiro, desempenho: permitir ordenar por qualquer campo significa permitir consultas que nenhum índice atende, como mostra o tópico de índices. Segundo, segurança: montar o ORDER BY com o texto vindo do cliente é injeção de SQL, já que a ordenação não pode ser passada como parâmetro da consulta. Mapeie o valor recebido para uma lista de colunas conhecidas.
Para o cursor funcionar com ordenações diferentes, ele precisa guardar os valores da coluna escolhida e do desempate, e a consulta precisa de um índice para cada ordenação oferecida. Por isso as APIs que usam cursor costumam oferecer poucas ordenações.
O total
| Opção | Custo | Quando usar |
|---|---|---|
| Não devolver | Nenhum | Feeds, rolagem infinita, APIs de sincronização |
COUNT(*) a cada página | Alto: 67 ms em 2 milhões de linhas, no teste | Tabelas pequenas, ou com filtros muito seletivos |
Contagem só sob pedido (?incluirTotal=true) | Pago só por quem precisa | Telas que exibem o total |
| Estimativa do banco | Muito baixo | "Cerca de 2 milhões de resultados" |
| Contagem em cache | Baixo | Totais que podem estar alguns minutos desatualizados |
Resumo da decisão
- Cursor por padrão; deslocamento só em telas internas, com limite de profundidade.
- Desempate único na ordenação, e índice que a cubra.
- Cursor opaco, com a precisão do banco, validado ao chegar.
- Tamanho padrão e máximo em toda listagem.
- Filtros e ordenações de uma lista fechada, cada um com índice.
- Total só quando necessário, e de forma barata.
O próximo nível mostra a paginação por cursor nas cinco linguagens, executada contra o PostgreSQL percorrendo todas as páginas de uma tabela.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Desenhar a listagem da minha API
Quero desenhar as rotas de listagem da minha API: paginação, filtros, ordenação e total. Pergunte qual banco eu uso, quais listas a API oferece, o tamanho esperado de cada uma e quem as consome (telas, integrações, sincronização). Para cada lista, recomende: 1. Deslocamento ou cursor, e por quê. 2. Os filtros e ordenações a oferecer, e os índices necessários. 3. O tamanho padrão e o máximo. 4. Se e como devolver o total. 5. O formato da resposta e do cursor. Termine com a convenção que todas as listas da API devem seguir.
Migrar de OFFSET para cursor
Minha API usa paginação por OFFSET e as páginas finais estão lentas. Quero migrar para cursor sem quebrar os clientes. Pergunte qual linguagem, banco e framework eu uso, e quem consome a API. Explique: 1. Como medir o problema atual: tempo por profundidade de página. 2. Como implementar o cursor ao lado do parâmetro atual, sem remover o antigo. 3. Os índices necessários. 4. Como comunicar a mudança aos clientes e limitar a profundidade do OFFSET. 5. Como testar que a nova paginação percorre todos os itens sem repetir nem pular.
Filtros e ordenação seguros
Quero implementar filtros e ordenação na minha API sem abrir brechas de segurança nem consultas lentas. Pergunte qual linguagem, framework e banco eu uso. Explique: 1. Por que a ordenação não pode ser um parâmetro da consulta e como montar o ORDER BY a partir de uma lista fechada. 2. Como validar filtros e devolver 400 com erros claros. 3. Como garantir que cada combinação de filtro e ordenação tenha índice. 4. Como lidar com filtros de texto parcial sem curinga no início. 5. Como combinar filtros com a paginação por cursor.