Aprenda / Ruby on Rails

IntermediárioAula 14 de 2015 min0% da trilha

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.

app/controllers/produtos_controller.rb
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
end
GET /produtos?page=2 — Resultado
pagy(Produto.all)

Filtro por query param — sem confiar cegamente no que chega

filtro combinado com paginação
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 }
  }
end

where(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

NUNCA faça isso
Produto.where("nome LIKE '%#{params[:busca]}%'")   # interpola direto na string SQL

Se 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:

correto — placeholder escapa 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.

Quiz rápido

Por que uma listagem de API em produção não deveria simplesmente devolver Model.all sem paginação?
Qual o problema de escrever Produto.where("nome LIKE '%#{params[:busca]}%'") com interpolação direta de string?