Paginação e filtros de API
Devolver a tabela inteira numa resposta funciona até ela ter volume real — paginação e filtro seguro resolvem isso antes que vire incidente.
Produto.all funciona perfeitamente com 20 registros de teste. Com 200 mil em produção, essa mesma linha devolve um JSON enorme, deixa a resposta lenta e pode até pressionar a memória do processo à toa — o cliente quase nunca precisa da tabela inteira de uma vez. Toda listagem pensada pra produção precisa de paginação desde o início, não como otimização de depois.
Pagy — paginação sem acoplar a view nenhuma
Pagy é a gem de paginação usada no padrão: calcula offset e limite a partir da página pedida e devolve metadados (página atual, total de páginas, total de registros), sem assumir nada sobre HTML — o que importa numa API que só devolve JSON.
class ProdutosController < ApplicationController
include Pagy::Backend
def index
pagy, produtos = pagy(Produto.all)
render json: {
data: ProdutoSerializer.new(produtos).serializable_hash[:data],
meta: { pagina: pagy.page, total_paginas: pagy.pages, total_registros: pagy.count }
}
end
endpagy(Produto.all)Filtro por query param — sem confiar cegamente no que chega
def index
produtos = Produto.all
produtos = produtos.where(categoria_id: params[:categoria_id]) if params[:categoria_id].present?
pagy, produtos_paginados = pagy(produtos)
render json: {
data: ProdutoSerializer.new(produtos_paginados).serializable_hash[:data],
meta: { pagina: pagy.page, total_paginas: pagy.pages }
}
endwhere(categoria_id: params[:categoria_id]) é seguro porque ActiveRecord monta a query com parâmetro escapado — o valor de params[:categoria_id] nunca vira parte literal do SQL, não importa o que venha nele.
O erro que abre brecha de SQL injection
Produto.where("nome LIKE '%#{params[:busca]}%'") # interpola direto na string SQLSe params[:busca] chegar como ' OR '1'='1, o texto interpolado muda o significado da query inteira, não só o valor buscado — isso é injeção de SQL. A correção é usar placeholder, deixando o ActiveRecord escapar o valor:
Produto.where("nome LIKE ?", "%#{params[:busca]}%")Exercício
Adicione ao index de PedidosController um filtro opcional por status (via ?status=finalizado), combinado com a paginação já existente, sem interpolar nada direto numa string SQL.