Filtragem, ordenação e paginação
Os bugs de endpoints de listagem que ninguém nota até a página 3.
Um endpoint de listagem parece simples: você pede produtos e recebe produtos. Aí a lista cresce além do que cabe numa resposta, os usuários querem ordenar por preço, filtrar por categoria e buscar pelo nome, e cada uma dessas funcionalidades é um pedacinho de código com os próprios casos extremos. Bugs de listagem são traiçoeiros porque a resposta sempre parece plausível. Uma página com os dez produtos errados continua sendo uma página com dez produtos.
Esta lição usa /api/Products, que tem 20 produtos nos dados iniciais, e os endpoints de demonstração em /api/paginationdocs. Os mesmos parâmetros funcionam em toda listagem desta API, e a referência da API os descreve.
| Parâmetro | O que faz aqui |
|---|---|
limit | Tamanho da página, de 1 a 100. Sem ele, você recebe tudo. |
page | Número da página começando em 1, usado com limit. |
offset | Pula esse número de registros, como alternativa ao page. |
sort, order | Ordena por qualquer campo, asc (o padrão) ou desc. |
q | Busca sem diferenciar maiúsculas em campos de texto como title e description. |
qualquer nome de campo | Filtro exato sem diferenciar maiúsculas, como ?category=books. |
Paginação
Peça três produtos por página e olhe os cabeçalhos além do corpo:
/api/Products?limit=3&page=1Vinte produtos em páginas de três dão sete páginas, e a última tem dois. A última página é o primeiro lugar para procurar bugs, porque é o único caso em que a página não está cheia:
$ curl -i "https://api.testingapis.com/api/Products?limit=3&page=7"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
x-page: 7
x-per-page: 3
x-total-count: 20
x-total-pages: 7
[{"id":19,"title":"Non-Slip Yoga Mat",...},{"id":20,"title":"Resistance Bands Set",...}]Correto: sete páginas, e a última tem os produtos 19 e 20. Um bug clássico aqui é calcular o número de páginas com divisão inteira (20 / 3 = 6), o que esconde os dois últimos produtos de quem vai passando as páginas.
Valores fora da faixa
Em seguida, envie valores que a documentação diz serem inválidos, ou que ela nem menciona. É isto que a API faz com eles:
?limit=3&page=8 200, x-page: 8, body []
?limit=3&page=0 200, x-page: 1, same as page 1
?limit=0 200, x-per-page: 1
?limit=-1 200, x-per-page: 1
?limit=101 200, x-per-page: 100
?limit=abc 200, x-per-page: 20 (limit ignored, all 20 returned)O servidor nunca recusa. Ele ajusta um valor ruim para o válido mais próximo, ou o ignora. É um projeto legítimo, e amigável com os clientes, mas um teste deve registrá-lo, porque a alternativa (um 400 que explica a faixa válida) é igualmente comum. Os casos perigosos são aqueles em que o ajuste esconde um bug do cliente. Um cliente que manda limit=abc por causa de um erro de template recebe aqui todos os 20 registros. Numa tabela com dois milhões de linhas, o mesmo comportamento significa que o servidor tenta devolver todas.
Uma página além do fim devolve uma lista vazia com 200. É o que a maioria das APIs faz, e é o que os clientes esperam quando repetem até não receber mais nada. Um 404 para uma página vazia também seria defensável. Um 500 não.
Sem buracos, sem duplicados
O teste de paginação mais importante também é o mais simples de descrever: percorra todas as páginas, junte os IDs e confira se recebeu cada registro exatamente uma vez. Compare a quantidade que juntou com o X-Total-Count.
async function allPages(path, limit) {
const seen = [];
for (let page = 1; ; page++) {
const res = await fetch(`https://api.testingapis.com${path}?limit=${limit}&page=${page}`);
const items = await res.json();
if (items.length === 0) break;
seen.push(...items.map((item) => item.id));
}
return seen;
}
const ids = await allPages("/api/Products", 3);
const total = 20; // from X-Total-Count
console.log(ids.length === total, new Set(ids).size === ids.length);Rode com vários tamanhos de página: 1, 3, 7 e um maior que a lista inteira. Erros de um a mais ou a menos nos cálculos de deslocamento costumam aparecer só em tamanhos específicos. Se a API aceita tanto page quanto offset, confira se limit=5&page=3 e limit=5&offset=10 devolvem os mesmos registros.
Este teste só passa de forma confiável se ninguém mudar os dados enquanto ele roda. Num servidor compartilhado como este, alguém pode mudar. Isso leva à principal fraqueza dos números de página, tratada mais abaixo.
Ordenação
Ordene por preço, do maior para o menor, e confira a ordem do resultado inteiro em vez de só o primeiro item:
/api/Products?sort=price&order=desc&limit=5Os cinco primeiros voltam como 249.99, 199.99, 89.99, 89.99, 59.99. Os produtos 3 e 8 custam o mesmo. Empates importam para a paginação: se o servidor não desempata de um jeito fixo, o mesmo produto pode aparecer no fim da página 1 e no começo da página 2, ou em nenhuma das duas. A maioria dos bancos de dados não promete nada sobre a ordem de linhas iguais, a não ser que a consulta acrescente uma segunda chave de ordenação, geralmente o ID.
Ordenar por categoria mostra como esta API lida com isso. Dentro de cada categoria, os produtos ficam em ordem de ID, tanto crescente quanto decrescente:
?sort=category
10:books 11:books 12:books 13:books 6:clothing 7:clothing 8:clothing 9:clothing
1:electronics 2:electronics 3:electronics 4:electronics 5:electronics 14:home ...
?sort=category&order=desc
18:sports 19:sports 20:sports 14:home 15:home 16:home 17:home 1:electronics ...Isso é estável, que é o que você quer. Para testar, ordene por um campo com muitos valores repetidos, busque a lista duas vezes (e percorra-a por páginas com um limite pequeno) e confira se a ordem nunca muda.
Mais casos de ordenação para testar em qualquer API:
- Strings com maiúsculas e minúsculas misturadas. "apple" vem antes ou depois de "Banana"?
- Números guardados como strings, em que "10" vem antes de "9".
- Registros em que o campo de ordenação é null ou está ausente. Eles vão para o início, para o fim, ou somem?
- Datas em fusos horários diferentes.
Parâmetros que são ignorados
O que acontece quando você erra um parâmetro dá mais problema do que qualquer uma das funcionalidades em si:
?sort=colour&limit=5 200, ids 1,2,3,4,5 (unsorted)
?sort=price&order=sideways 200, sorted ascending
?categry=books 200, all 20 products
?rating.rate=4.3 200, all 20 productsTodos devolvem 200 e uma lista que parece razoável. Uma ordenação por um campo que não existe é descartada. Uma ordem sideways vira crescente. Um filtro escrito errado, categry, é ignorado, então o cliente pediu livros e recebeu todos os 20 produtos. Filtrar por um campo aninhado com notação de pontos não é suportado, e isso também é ignorado.
Na prática, o filtro escrito errado é o pior deles. Pense numa tela que mostra "pedidos do cliente 42", construída sobre um parâmetro de filtro que alguém renomeou. Depois da renomeação, todo cliente vê todos os pedidos, e a API devolve 200 o tempo todo. Ao testar filtros, inclua sempre um caso em que o filtro deveria remover a maioria dos registros, e confira se removeu. Um teste que só confere que "todo produto devolvido é da categoria books" também passa com uma lista vazia, então confira a contagem também.
Filtros e busca
Nesta API os filtros são exatos e não diferenciam maiúsculas: ?category=Electronics devolve os cinco produtos de eletrônicos, e ?category=electron devolve uma lista vazia. Campos booleanos funcionam do mesmo jeito:
/api/Products?inStock=falseA busca com qprocura trechos do texto. É aqui que a codificação começa a importar, porque termos de busca têm espaços, &, sinais de porcentagem e aspas, e todos eles têm significado especial numa URL:
?q=YOGA 19
?q=yoga+mat 19
?q=yoga%20mat 19
?q=100%25 6 (description contains "100% organic cotton")
?q=%27 11,13 (titles and descriptions with an apostrophe)
?q= all 20
?q=a&q=b []
?category=books&category=home []Espaços funcionam escritos como + ou como %20. Um % literal precisa ser enviado como %25, e ele encontra corretamente a camiseta de "100% organic cotton". Uma busca por um apóstrofo funciona e devolve dois livros, um pequeno sinal de que a busca não monta SQL colando strings (a lição de segurança explica por que isso importa). Se você mandar um espaço sem codificar, o curl se recusa a enviar a requisição.
As duas últimas linhas merecem uma nota. Repetir um parâmetro é permitido numa URL, e as APIs tratam isso de jeitos diferentes: o primeiro valor vence, o último valor vence, os dois valores são combinados com OR, ou são unidos numa string só. Aqui, repetir q ou category devolve uma lista vazia, o que sugere que os valores foram unidos em algo como books,home, que não combina com nada. Se a interface do seu produto um dia deixar as pessoas escolherem duas categorias, há um bug esperando para ser encontrado aqui.
Combinando tudo
Filtre, ordene e pagine numa requisição só, e confira se tudo é aplicado na ordem certa: primeiro o filtro, depois a ordenação, depois a página.
/api/Products?category=electronics&sort=price&order=desc&limit=2&page=2A página 2 tem os produtos 3 e 4 (89.99 e 59.99), e o X-Total-Counté 5, o número de produtos de eletrônicos, não 20. Se o total ignorasse o filtro, um cliente mostraria "página 1 de 10" para uma lista de três páginas. Esse erro é comum, porque os totais muitas vezes são contados por uma consulta separada que alguém esqueceu de atualizar.
Por que os números de página quebram
Números de página e offsets descrevem uma posição na lista: "pule 20, me dê 10". Isso funciona para dados que não mudam. Quando mudam, as posições se deslocam:
- Você busca a página 1 (itens 1 a 10) de uma lista ordenada do mais novo para o mais antigo.
- Alguém cria um item novo. Ele vai para o topo, e tudo desce uma posição.
- Você busca a página 2 (posições 11 a 20). A posição 11 agora é o item que era o 10, então você o vê duas vezes.
Apagar um item faz o contrário: tudo sobe uma posição e um item nunca é mostrado. Num feed de notícias, é um pequeno incômodo. Num processo que exporta todas as faturas percorrendo páginas, significa que uma fatura falta na exportação e ninguém percebe.
A paginação por cursor evita isso descrevendo onde você parou em vez de quanto já andou. Cada resposta entrega um token para a próxima requisição:
/api/pagination/cursor?limit=3{
"object": "list",
"url": "/api/pagination/cursor",
"data": [
{ "id": 1, "name": "Smart Headset", "category": "electronics", "price": 42.01, "createdAt": "2026-01-01T21:00:00Z" },
{ "id": 2, "name": "Compact Speaker", "category": "outdoors", "price": 79.02, "createdAt": "2026-01-02T10:00:00Z" },
{ "id": 3, "name": "Deluxe Blender", "category": "office", "price": 116.03, "createdAt": "2026-01-02T23:00:00Z" }
],
"has_more": true,
"next_cursor": "eyJsYXN0SWQiOjN9"
}Você devolve o next_cursor como ?cursor= e continua até has_moreser false. Um cursor inválido recebe um 400 claro: "Invalid cursor. Pass the 'next_cursor' value from a previous response unchanged."
A paginação keyset é a mesma ideia sem o embrulho. O cliente envia o último ID que viu, e o servidor devolve os registros depois dele:
/api/pagination/keyset?after_id=10&limit=3{
"data": [
{ "id": 11, "name": "Smart Lamp", "category": "electronics", "price": 12.11, "createdAt": "2026-01-07T07:00:00Z" },
{ "id": 12, "name": "Compact Headset", "category": "outdoors", "price": 49.12, "createdAt": "2026-01-07T20:00:00Z" },
{ "id": 13, "name": "Deluxe Speaker", "category": "office", "price": 86.13, "createdAt": "2026-01-08T09:00:00Z" }
],
"has_more": true,
"next_after_id": 13
}O terceiro estilo coloca os links de página num cabeçalho Link, como faz a API do GitHub. O corpo continua sendo um array simples:
$ curl -i "https://api.testingapis.com/api/pagination/link?page=2&per_page=3"
HTTP/1.1 200 OK
link: <http://api.testingapis.com/api/pagination/link?page=3&per_page=3>; rel="next",
<http://api.testingapis.com/api/pagination/link?page=34&per_page=3>; rel="last",
<http://api.testingapis.com/api/pagination/link?page=1&per_page=3>; rel="first",
<http://api.testingapis.com/api/pagination/link?page=1&per_page=3>; rel="prev"
x-total-count: 100O número de páginas está certo (100 itens em páginas de 3 dão 34 páginas). Mas olhe as URLs. A requisição foi para https, e todos os links apontam para http. Esse é um bug real, e comum: a aplicação roda atrás de um proxy que cuida do HTTPS, ela mesma vê HTTP simples e monta os links a partir do que vê. Um cliente que segue esses links ou é redirecionado ou, numa configuração mais rígida, manda a próxima requisição sem criptografia. Conferir o esquema e o host de toda URL que uma API gera é um teste barato que pega isso.
Para cada estilo de paginação, os testes centrais são os mesmos: percorra até o fim, junte os IDs, procure buracos e duplicados, e confira o que acontece na última página. Para cursores e keysets, adicione um registro enquanto percorre e confirme que continua vendo cada registro original exatamente uma vez.
Exercício
Escolha /api/Booksentre para ver e anote os resultados de cada passo.
- Descubra quantos livros existem pelo
X-Total-Counte percorra a lista comlimit=4. Você recebeu cada ID uma vez? - Ordene por
genre. Busque duas vezes e comlimit=3pelas páginas. A ordem dentro de cada gênero é estável? - Filtre por um gênero, ordene por
pricedo maior para o menor e peça a página 2. OX-Total-Counté a contagem filtrada? - Escreva errado o nome do filtro. O que volta?
- Percorra
/api/pagination/offsetentre para ver e/api/pagination/linkentre para ver até o fim. Onde eles sinalizam a última página, e os dois concordam no total?
Para cada surpresa, decida se é um bug, uma lacuna na documentação ou uma escolha de projeto que você ainda questionaria.