🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Como implementar paginação em uma API REST: guia completo de 2026

⏱️6 min read  ·  1,203 words

Paginação é essencial para qualquer API que retorne listas — sem ela, uma consulta que retorna milhões de linhas trava seu servidor e sobrecarrega os clientes. Mas existem diversas estratégias de paginação com características de desempenho muito diferentes. Este guia cobre todos eles e quando usar cada um.

Por que a paginação é importante

  • Desempenho: Retornar todas as linhas é lento e consome muita memória
  • Largura de banda: Os clientes não precisam de milhares de registros de uma vez
  • Carregamento do banco de dados: Consultas limitadas protegem seu banco de dados
  • Experiência do usuário: O carregamento de dados nas páginas é mais rápido e limpo

Estratégia 1: Paginação de deslocamento/limite (simples)

-- The classic approach: OFFSET and LIMIT
SELECT * FROM products
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;   -- page 3 (skip 40, take 20)
// Express endpoint
app.get('/products', async (req, res) => {
  const page  = parseInt(req.query.page)  || 1;
  const limit = parseInt(req.query.limit) || 20;
  const offset = (page - 1) * limit;

  const products = await db.query(
    'SELECT * FROM products ORDER BY created_at DESC LIMIT $1 OFFSET $2',
    [limit, offset]
  );
  const total = await db.query('SELECT COUNT(*) FROM products');

  res.json({
    data: products.rows,
    pagination: {
      page,
      limit,
      total: total.rows[0].count,
      totalPages: Math.ceil(total.rows[0].count / limit),
    }
  });
});

Prós: Simples, suporta pular para qualquer página, mostra a contagem total.
Contras: Lento em deslocamentos grandes (o banco de dados ainda verifica todas as linhas ignoradas) e os resultados podem mudar se os dados mudarem entre as solicitações.

Estratégia 2: Paginação do Cursor (Escalável)

Em vez de um deslocamento, use um “cursor” (geralmente o ID ou carimbo de data/hora do último item) para buscar a próxima página. Isso é dimensionado para milhões de linhas porque o banco de dados salta diretamente para a posição:

-- Fetch items AFTER a cursor (much faster than large OFFSET)
SELECT * FROM products
WHERE created_at < $1   -- cursor = last item's created_at
ORDER BY created_at DESC
LIMIT 20;
app.get('/products', async (req, res) => {
  const limit  = parseInt(req.query.limit) || 20;
  const cursor = req.query.cursor;   // the last item's timestamp/id

  let query, params;
  if (cursor) {
    query = 'SELECT * FROM products WHERE created_at < $1 ORDER BY created_at DESC LIMIT $2';
    params = [cursor, limit + 1];   // fetch one extra to check for more
  } else {
    query = 'SELECT * FROM products ORDER BY created_at DESC LIMIT $1';
    params = [limit + 1];
  }

  const result = await db.query(query, params);
  const hasMore = result.rows.length > limit;
  const items = hasMore ? result.rows.slice(0, limit) : result.rows;
  const nextCursor = hasMore ? items[items.length - 1].created_at : null;

  res.json({
    data: items,
    pagination: { nextCursor, hasMore }
  });
});

Prós: Rápido em qualquer escala (sem digitalização offset), resultados estáveis mesmo quando os dados mudam.
Contras: Não é possível pular para páginas arbitrárias, sem contagem total de páginas, um pouco mais complexo.

Estratégia 3: Paginação do conjunto de chaves (melhor desempenho)

-- Keyset uses a unique, ordered column (often id) for precise positioning
SELECT * FROM products
WHERE (created_at, id) < ($1, $2)   -- composite cursor handles ties
ORDER BY created_at DESC, id DESC
LIMIT 20;

A paginação do conjunto de chaves usa um cursor composto (como create_at + id) para lidar com linhas com carimbos de data/hora idênticos. É a abordagem mais robusta e de alto desempenho, usada por APIs que atendem grandes conjuntos de dados.

Comparação: qual usar

Estratégia Melhor para Escala
Deslocamento/Limite Conjuntos de dados pequenos, UIs administrativas que precisam de números de página Pequeno-médio
Cursor Feeds, rolagem infinita, grandes conjuntos de dados Grande
Conjunto de chaves Grandes conjuntos de dados, necessidades de alto desempenho Muito grande

Formato de resposta consistente

// Offset-based response
{
  "data": [ /* items */ ],
  "pagination": {
    "page": 3,
    "limit": 20,
    "total": 1543,
    "totalPages": 78
  }
}

// Cursor-based response
{
  "data": [ /* items */ ],
  "pagination": {
    "nextCursor": "2026-07-20T10:30:00Z",
    "hasMore": true
  }
}

Dicas de desempenho

  • Indexe sua coluna de classificação: Consultas de paginação precisam de um índice na coluna ORDER BY ou são lentas
  • Evite COUNT(*) em tabelas grandes: Contar todas as linhas é caro — a paginação do cursor evita a necessidade de uma contagem total
  • Limite o limite: Imponha um tamanho máximo de página (por exemplo, 100) para que os clientes não possam solicitar tudo
  • Use cursor/conjunto de chaves para dados grandes: A paginação de deslocamento degrada muito em deslocamentos grandes — o banco de dados verifica todas as linhas ignoradas
  • Buscar limit+1 para detectar “hasMore”: Solicite uma linha extra para saber se há uma próxima página sem uma contagem separada

Perguntas Frequentes

P: Paginação por deslocamento ou cursor?
R: Compensação para pequenos conjuntos de dados e UIs administrativas que precisam de números de página e de pular para páginas específicas. Cursor para feeds, rolagem infinita e grandes conjuntos de dados onde o desempenho e a estabilidade são importantes. O cursor é dimensionado muito melhor, mas não pode pular para páginas arbitrárias.

P: Por que minha paginação offset está lenta?
A: OFFSET 100000 faz com que o banco de dados escaneie e descarte 100.000 linhas antes de retornar sua página – cada vez mais lento à medida que o deslocamento aumenta. Mude para a paginação do cursor ou do conjunto de chaves, que vai diretamente para a posição usando uma coluna indexada.

P: Como lidar com a alteração de dados entre páginas?
R: A paginação de deslocamento pode pular ou duplicar itens se linhas forem adicionadas/removidas entre solicitações. A paginação do cursor é estável porque ancora na posição de um item específico, não em um deslocamento numérico. Use a paginação do cursor quando os dados mudam com frequência.

P: Preciso retornar uma contagem total?
R: Para paginação offset com números de página, geralmente sim. Mas COUNT(*) em tabelas grandes é caro. A paginação do cursor evita isso completamente (apenas “hasMore”). Se você precisar de contagens aproximadas em tabelas enormes, considere contagens em cache ou estimadas.

P: Qual é um bom tamanho de página padrão e máximo?
R: 20-25 como padrão, com um máximo de 100 aplicados no lado do servidor. Isso equilibra o tamanho da resposta e o número de solicitações. Sempre limite o limite para que os clientes não possam solicitar dados ilimitados com?limit=999999.

Conclusão

A paginação adequada é essencial para qualquer API de retorno de lista. Usardeslocamento/limite para pequenos conjuntos de dados e UIs administrativas que precisam de números de página, paginação de cursor para feeds e grandes conjuntos de dados e paginação de conjunto de chaves para necessidades massivas de alto desempenho. O deslocamento é mais simples, mas degrada em deslocamentos grandes; o cursor e o conjunto de chaves são dimensionados para milhões de linhas, saltando diretamente para a posição usando colunas indexadas. Indexe sua coluna de classificação, limite o tamanho da página, busque limite+1 para detectar mais páginas e retorne um formato de resposta consistente. Escolher a estratégia certa para sua escala de dados mantém sua API rápida e seu banco de dados saudável à medida que seus dados crescem.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা