1. Paginação — nunca devolva tudo de uma vez
Um endpoint de listagem sem paginação funciona bem com 50 registros e vira um problema sério com 50 mil. O padrão mais comum é paginação por página/tamanho:
public record PaginaResultado<T>(List<T> Itens, int Pagina, int TamanhoPagina, int Total);
[HttpGet]
public async Task<ActionResult<PaginaResultado<ClienteDto>>> Listar(
[FromQuery] int pagina = 1,
[FromQuery] int tamanhoPagina = 20)
{
if (tamanhoPagina > 100) tamanhoPagina = 100; // limite máximo, protege contra abuso
var (itens, total) = await _repositorio.ListarPaginadoAsync(pagina, tamanhoPagina);
return Ok(new PaginaResultado<ClienteDto>(
itens.Select(ClienteMapper.ParaDto).ToList(), pagina, tamanhoPagina, total));
}
CREATE OR ALTER PROCEDURE dbo.usp_ClienteListarPaginado
@Pagina INT = 1,
@TamanhoPagina INT = 20
AS
BEGIN
SET NOCOUNT ON;
SELECT Id, Nome, Email
FROM dbo.Cliente
ORDER BY Nome
OFFSET (@Pagina - 1) * @TamanhoPagina ROWS
FETCH NEXT @TamanhoPagina ROWS ONLY;
SELECT COUNT(*) AS Total FROM dbo.Cliente;
END;
OFFSET/FETCH é a forma padrão do T-SQL de paginar diretamente no
banco — muito mais eficiente que trazer todas as linhas para o C# e paginar em memória com LINQ,
especialmente numa tabela grande.
2. Filtros e ordenação como query string
// GET /api/clientes?nome=ana&ativo=true&ordenarPor=nome&pagina=2
[HttpGet]
public async Task<ActionResult> Listar(
[FromQuery] string? nome,
[FromQuery] bool? ativo,
[FromQuery] string ordenarPor = "nome",
[FromQuery] int pagina = 1)
{
...
}
Isso mapeia diretamente para os parâmetros opcionais das procedures de listagem que você já escreveu no Módulo 2 (capítulo 3) e o SQL dinâmico de ordenação (Módulo 2, capítulo 10) — a API é, em boa parte, uma tradução direta desses conceitos para query string.
3. CORS — permitindo que um front-end separado consuma a API
Se a API é consumida por um front-end rodando em outro domínio/porta (um SPA React/Vue, por exemplo), o navegador bloqueia a chamada por padrão, a menos que o servidor autorize explicitamente via CORS (Cross-Origin Resource Sharing):
builder.Services.AddCors(opcoes =>
{
opcoes.AddPolicy("FrontEnd", politica =>
politica.WithOrigins("https://meusite.com")
.AllowAnyMethod()
.AllowAnyHeader());
});
var app = builder.Build();
app.UseCors("FrontEnd");
Evite AllowAnyOrigin() em produção, especialmente combinado com
AllowCredentials() — isso permite que qualquer site na internet faça
requisições autenticadas à sua API em nome do usuário. Liste os domínios exatos que devem ter
acesso.
4. Convenções que tornam uma API previsível
- Nomes de recursos sempre no plural:
/clientes, não/cliente. - camelCase consistente no JSON (o padrão já configurado, capítulo 3).
- Datas em formato ISO 8601 (
2026-01-15T10:30:00Z) — sem ambiguidade de fuso ou formato regional. - Erros sempre no mesmo formato (
ProblemDetails, capítulo 6), em qualquer endpoint. - Filtros e paginação com nomes de parâmetro consistentes em toda a API (sempre
pagina/tamanhoPagina, nuncapagenum endpoint epaginaem outro).
5. Rate limiting — protegendo contra abuso
builder.Services.AddRateLimiter(opcoes =>
{
opcoes.AddFixedWindowLimiter("padrao", janela =>
{
janela.PermitLimit = 100;
janela.Window = TimeSpan.FromMinutes(1);
});
});
app.UseRateLimiter();
6. Onde isso te leva
Com boas práticas de design cobertas, o último capítulo deste módulo junta tudo — paginação, autenticação, tratamento de erro, documentação — numa API completa sobre o sistema de Pedidos do Módulo 2.