Projeto guiado: evoluindo a API de Pedidos
A mesma API de Pedidos da lição de fundamentos ganha paginação, envelope de resposta, job assíncrono e teste de request — fechando fundamentos e intermediário juntos.
O projeto guiado de fundamentos deixou o recurso Pedido com scaffold, validação, Ability, service, serializer e controller autenticado/autorizado. Agora cada peça evolui com o que as lições intermediárias acrescentaram: index com paginação, resposta em envelope com erro tratado centralmente, o service devolvendo um Resultado em vez de true/false e disparando um job assíncrono, e um teste de request cobrindo o fluxo inteiro.
1. Paginação no index
class PedidosController < ApplicationController
include Pagy::Backend
before_action :authenticate_usuario!
load_and_authorize_resource
def index
pagy, pedidos = pagy(current_usuario.pedidos.order(created_at: :desc))
render json: {
data: PedidoSerializer.new(pedidos).serializable_hash[:data],
meta: { pagina: pagy.page, total_paginas: pagy.pages, total_registros: pagy.count }
}
end
end2. Envelope de resposta e rescue_from central
class ApplicationController < ActionController::API
rescue_from ActiveRecord::RecordNotFound, with: :registro_nao_encontrado
rescue_from CanCan::AccessDenied, with: :acesso_negado
private
def registro_nao_encontrado(excecao)
render json: { error: { codigo: "nao_encontrado", mensagem: excecao.message } }, status: :not_found
end
def acesso_negado(_excecao)
render json: { error: { codigo: "acesso_negado", mensagem: "Você não tem permissão para essa ação" } },
status: :forbidden
end
end3. Service com Resultado, disparando job assíncrono
class Resultado
attr_reader :erros, :pedido
def initialize(sucesso:, pedido: nil, erros: [])
@sucesso = sucesso
@pedido = pedido
@erros = erros
end
def sucesso?
@sucesso
end
endclass FinalizarPedido
def initialize(pedido)
@pedido = pedido
end
def call
return Resultado.new(sucesso: false, erros: ["Pedido sem itens"]) unless @pedido.itens.any?
ActiveRecord::Base.transaction do
@pedido.update!(status: :finalizado)
@pedido.itens.each { |item| item.produto.decrement!(:estoque, item.quantidade) }
end
PedidoMailer.confirmacao(@pedido).deliver_later # roda fora do request, via Solid Queue
Resultado.new(sucesso: true, pedido: @pedido)
end
end"Pedido sem itens" continua sendo um resultado esperado, não uma exception — e o envio do e-mail de confirmação não bloqueia mais a resposta, porque roda em background depois que a transação já foi confirmada.
4. Controller final — todas as peças juntas
class PedidosController < ApplicationController
include Pagy::Backend
before_action :authenticate_usuario!
load_and_authorize_resource
def index
pedidos = current_usuario.pedidos.order(created_at: :desc)
pedidos = pedidos.where(status: params[:status]) if params[:status].present?
pagy, pedidos_paginados = pagy(pedidos)
render json: {
data: PedidoSerializer.new(pedidos_paginados).serializable_hash[:data],
meta: { pagina: pagy.page, total_paginas: pagy.pages, total_registros: pagy.count }
}
end
def create
pedido = current_usuario.pedidos.new(pedido_params)
if pedido.save
render json: { data: PedidoSerializer.new(pedido).serializable_hash[:data] }, status: :created
else
render json: { error: { codigo: "invalido", mensagens: pedido.errors.full_messages } },
status: :unprocessable_entity
end
end
def finalizar
resultado = FinalizarPedido.new(@pedido).call
if resultado.sucesso?
render json: { data: PedidoSerializer.new(resultado.pedido).serializable_hash[:data] }
else
render json: { error: { codigo: "invalido", mensagens: resultado.erros } }, status: :unprocessable_entity
end
end
private
def pedido_params
params.require(:pedido).permit(:total)
end
end5. Teste de request cobrindo o fluxo de finalizar
RSpec.describe "Pedidos", type: :request do
describe "POST /pedidos/:id/finalizar" do
it "finaliza o pedido e enfileira o e-mail de confirmação" do
usuario = Usuario.create!(email: "ana@exemplo.com", password: "12345678")
pedido = usuario.pedidos.create!(total: 150)
pedido.itens.create!(produto: Produto.create!(nome: "Teclado", preco: 100, estoque: 5), quantidade: 1)
expect {
post "/pedidos/#{pedido.id}/finalizar", headers: { "Authorization" => "Bearer #{token_para(usuario)}" }
}.to have_enqueued_mail(PedidoMailer, :confirmacao)
expect(response).to have_http_status(:ok)
expect(pedido.reload.status).to eq("finalizado")
end
it "devolve erro quando o pedido não tem itens" do
usuario = Usuario.create!(email: "bia@exemplo.com", password: "12345678")
pedido = usuario.pedidos.create!(total: 0)
post "/pedidos/#{pedido.id}/finalizar", headers: { "Authorization" => "Bearer #{token_para(usuario)}" }
expect(response).to have_http_status(:unprocessable_entity)
end
end
endhave_enqueued_mail confirma que o job de e-mail foi enfileirado — não que ele foi entregue. A entrega de fato roda depois, fora do ciclo do teste, processada pelo worker do Solid Queue; o request spec só precisa garantir que a chamada certa foi enfileirada.
Exercício
O index acima já filtra por status. Adicione também um filtro opcional por data mínima (?desde=2026-01-01), combinado com o filtro de status e a paginação já existentes, sem interpolar nada direto numa string SQL.