Do "funciona" ao "pronto para produção": pontos-chave de evolução em Ruby on Rails
Pontos práticos de arquitetura, tratamento de erros, consistência de dados, segurança, Docker e testes para evoluir de código que funciona para código pronto para produção em Rails.
Existe uma distância grande entre um endpoint Rails que funciona no ambiente de desenvolvimento e uma API pronta para rodar em produção. Essa distância normalmente não está no domínio do problema, está em como o código lida com erros, como as responsabilidades estão organizadas e o quão fácil é operar a aplicação depois que ela sai da sua máquina.
Este artigo reúne pontos práticos que ajudam nessa evolução: arquitetura, tratamento de exceções, consistência de dados, segurança e operabilidade via Docker. São pontos aplicáveis a qualquer projeto Rails, não a um cenário específico.
Premissa do artigo: maturidade técnica não é sinônimo de complexidade. É organização, robustez e operabilidade.
O que geralmente já funciona bem
Em código Rails que já resolve o problema principal, alguns pontos costumam estar presentes, e servem de base para os próximos passos:
- ▸Fluxo principal funcional e legível, com nomes de métodos claros.
- ▸Uso correto do ecossistema Rails: ActiveRecord para persistência, Sidekiq para processamento assíncrono.
- ▸Testes cobrindo o caminho feliz (happy path) das principais operações.
Reconhecer isso evita cair em over engineering: os próximos pontos não substituem essa base, eles constroem em cima dela. Funcionar é necessário, mas insuficiente.
Separação de responsabilidades
O problema do controller "gordo"
É comum o controller acumular responsabilidades demais: orquestração HTTP (params, status, render), regra de negócio, serialização da resposta e até chamadas a serviços externos. O resultado:
- ▸Fica difícil testar a regra de negócio isoladamente, sem passar por uma requisição HTTP.
- ▸A lógica não é reaproveitável em outros contextos (um job, um comando de console, uma segunda rota).
- ▸O controller cresce sem parar e se torna um ponto único de acoplamento.
Padrão recomendado
Uma separação simples e amplamente usada em apps Rails maduras:
Controller -> orquestra (params, status HTTP, chama o service)
Service -> regra de negócio (ex.: Orders::CreateService)
Serializer -> formato da resposta JSON
Model -> persistência, validações e comportamentos de domínioUma estrutura de pastas comum para isso:
app/
controllers/
models/
services/
orders/
create_service.rb
cancel_service.rb
serializers/
order_serializer.rb
jobs/Service object
A regra de negócio fica isolada, testável sem depender de uma requisição HTTP:
class Orders::CreateService
def self.call(user:, items:)
new(user: user, items: items).call
end
def initialize(user:, items:)
@user = user
@items = items
end
def call
ActiveRecord::Base.transaction do
order = user.orders.create!(status: :pending)
items.each { |item| order.order_items.create!(item) }
order.update!(total: order.order_items.sum { |i| i.price * i.quantity })
order
end
end
private
attr_reader :user, :items
endController enxuto
class OrdersController < ApplicationController
def create
order = Orders::CreateService.call(user: current_user, items: order_params[:items])
render json: OrderSerializer.new(order).as_json, status: :created
end
private
def order_params
params.require(:order).permit(items: [:product_id, :quantity])
end
endSerializer dedicado
class OrderSerializer
def initialize(order)
@order = order
end
def as_json
{
id: order.id,
status: order.status,
total: order.total,
items: order.order_items.includes(:product).map do |item|
{ product_id: item.product_id, quantity: item.quantity }
end,
}
end
private
attr_reader :order
end- ▸Actions com poucas linhas, o grosso da lógica mora fora do controller.
- ▸Um service por operação de negócio relevante.
- ▸Serializers dedicados (ou gems como blueprinter, jsonapi-serializer) para a resposta.
- ▸Strong parameters (params.permit) em vez de params.require espalhado pelo código.
Tratamento robusto de exceções e contratos de API
Um padrão recorrente em código Rails ainda imaturo: o fluxo de sucesso funciona bem, mas erros comuns caem em respostas inconsistentes ou em um 500 genérico.
| Situação | Comportamento comum | Comportamento esperado |
|---|---|---|
| Registro inexistente (find) | 500 (RecordNotFound) | 404 com JSON padronizado |
| Parâmetros inválidos ou ausentes | 400/500 inconsistente | 422 com mensagem clara |
| Erro de validação (save!) | 500 | 422 com detalhes do erro |
| Erro interno inesperado | stack trace exposto | 500 genérico + log estruturado |
Centralizar o tratamento de erros no ApplicationController evita repetir a mesma lógica em cada controller:
class ApplicationController < ActionController::API
rescue_from ActiveRecord::RecordNotFound do |e|
render json: { error: "Resource not found", detail: e.message }, status: :not_found
end
rescue_from ActionController::ParameterMissing do |e|
render json: { error: "Invalid request", detail: e.message }, status: :unprocessable_entity
end
rescue_from ActiveRecord::RecordInvalid do |e|
render json: {
error: "Validation failed",
details: e.record.errors.full_messages,
}, status: :unprocessable_entity
end
endErros específicos do domínio também merecem uma classe própria, em vez de reaproveitar exceções genéricas do Ruby:
class OrderNotCancelableError < StandardError; end
class ApplicationController < ActionController::API
rescue_from OrderNotCancelableError do
render json: { error: "Order cannot be canceled" }, status: :unprocessable_entity
end
endUm critério objetivo de robustez é ter testes cobrindo esses cenários de erro, verificando status HTTP e corpo da resposta, não só o caminho feliz.
Consistência de dados e regras de negócio
Um ponto comum: campos derivados (como o total de um pedido) recalculados manualmente em múltiplos lugares do código, sem transação.
- ▸Divergência entre os itens e o total exibido.
- ▸Condições de corrida em requisições concorrentes.
- ▸Dados inconsistentes em caso de falha parcial no meio da operação.
Duas abordagens resolvem isso. A primeira calcula o total sempre a partir dos itens, sem guardar um valor derivado:
def total
order_items.includes(:product).sum { |item| item.price * item.quantity }
endA segunda mantém o total denormalizado, mas com garantias de atomicidade:
ActiveRecord::Base.transaction do
order_item.save!
order.recalculate_total!
order.save!
endSobre modelagem: é comum um schema carregar colunas de um desenho anterior que não fazem mais sentido depois que o domínio evolui. Nesse caso, uma migration removendo as colunas obsoletas mostra que o schema acompanhou a evolução do modelo, deixar colunas mortas é uma dívida técnica silenciosa.
Segurança e boas práticas de entrada
Inconsistência comum: alguns controllers usam strong parameters e outros acessam params.require diretamente, sem validação centralizada.
private
def order_params
params.require(:order).permit(items: [:product_id, :quantity])
end| Camada | Responsabilidade |
|---|---|
| Model | Invariantes de dados (quantidade > 0, total >= 0) |
| Service | Regras do caso de uso (produto existe, pedido pode ser cancelado) |
| Controller | Formato da requisição (parâmetros obrigatórios e permitidos) |
Docker e operabilidade
Problemas recorrentes em ambientes Docker de projetos Rails:
- ▸Serviço da aplicação incompleto ou com um TODO no docker-compose.yml.
- ▸Sintaxe inválida em variáveis de ambiente (dois-pontos em vez de igual, por exemplo).
- ▸README priorizando o setup manual em vez do setup via Docker.
- ▸Falta o serviço de processamento assíncrono (ex.: Sidekiq) no compose.
- ▸Divergência entre grupos do Gemfile e o que o Dockerfile instala, por exemplo, uma gem de teste excluída de uma imagem que também precisa rodar os testes.
O critério de operabilidade é simples: outra pessoa consegue clonar o repositório e rodar isso:
docker compose up --build
# API disponível em http://localhost:3000
# worker de background jobs rodandoUm docker-compose completo normalmente cobre banco, cache/fila, aplicação e worker assíncrono:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: app_development
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7.0.15-alpine
web:
build: .
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
DATABASE_URL: postgresql://postgres:password@db:5432/app_development
REDIS_URL: redis://redis:6379/0
command: ./bin/rails server -b 0.0.0.0
worker:
build: .
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
environment:
DATABASE_URL: postgresql://postgres:password@db:5432/app_development
REDIS_URL: redis://redis:6379/0
command: bundle exec sidekiq- ▸docker compose config valida sem erros.
- ▸docker compose up --build sobe todos os serviços.
- ▸Healthcheck no banco de dados.
- ▸Worker assíncrono incluído e conectado à fila.
- ▸README documenta o Docker antes do setup manual.
- ▸Testado em uma máquina limpa, sem depender de configuração local prévia.
Testes além do happy path
É comum a cobertura de testes ficar concentrada no caminho de sucesso das operações principais, deixando de fora erros e casos de borda.
| Área | Cobertura comum | Cobertura desejável |
|---|---|---|
| Operação principal (sucesso) | cobre | cobre |
| Registro inexistente | não cobre | cobre, 404 |
| Parâmetros inválidos | não cobre | cobre, 422 |
| Jobs assíncronos | cobertura básica | cobre + edge cases |
| Services isolados (sem HTTP) | não cobre | cobre |
Uma pirâmide prática para uma API Rails:
- ▸Request specs, integração via HTTP, cobrindo os endpoints.
- ▸Service specs, regra de negócio isolada, sem depender de uma requisição.
- ▸Model/Job specs, validações, comportamentos de domínio e jobs assíncronos.
Casos de borda que costumam faltar:
- ▸Coleção vazia após remover o último item.
- ▸Registro relacionado inexistente ao tentar associar algo a ele.
- ▸Quantidade zero ou negativa em um campo numérico, aplicar um default ou retornar 422.
- ▸Comportamento de jobs agendados, como expiração por inatividade.
Observabilidade e manutenção
Itens que elevam a maturidade de uma aplicação Rails além do escopo mínimo funcional:
- ▸Logs estruturados por operação relevante do domínio.
- ▸Retry limitado e tratamento de dead letter em jobs assíncronos.
- ▸Health check documentado no README.
- ▸Documentação de API (OpenAPI/Swagger ou README detalhado com exemplos de request/response).
- ▸CI rodando testes e lint automaticamente em cada push.
Checklist de evolução
Arquitetura
- ▸Controllers enxutos, com orquestração HTTP separada da regra de negócio.
- ▸Um service por caso de uso relevante.
- ▸Serializers dedicados para a resposta JSON.
- ▸Models com validações e comportamentos de domínio.
API
- ▸Erros padronizados (404, 422, 500).
- ▸rescue_from centralizado no ApplicationController.
- ▸Strong parameters em todos os endpoints.
- ▸Status HTTP corretos em cada resposta.
Dados
- ▸Transações em operações compostas por múltiplos passos.
- ▸Modelagem limpa, sem colunas legadas de um design anterior.
- ▸Fonte da verdade clara para campos calculados ou derivados.
Operação
- ▸docker compose up --build funcional do zero.
- ▸Serviços de banco, cache/fila e worker orquestrados.
- ▸README reproduzível por outra pessoa.
- ▸Exemplos de uso da API (curl ou coleção Postman).
Qualidade
- ▸Testes do caminho de sucesso.
- ▸Testes de erro (404, 422).
- ▸Testes de jobs assíncronos.
- ▸Solução validada em ambiente limpo, sem depender de configuração local.
Conclusão
Esses pontos não medem só a capacidade de fazer algo funcionar, medem a maturidade de entregar código que outra pessoa consegue operar com confiança: revisar, dar manutenção, escalar e depurar sem depender de quem escreveu o código originalmente.
Três pilares resumem essa evolução:
- ▸Arquitetura, separar responsabilidades entre controller, service, serializer e model.
- ▸Robustez, tratar erros como parte do contrato da API, não como exceção rara.
- ▸Operabilidade, Docker funcional, README reproduzível e testes cobrindo os casos de borda.
Referências
- ▸Rails Guides, Active Record Basics: https://guides.rubyonrails.org/active_record_basics.html
- ▸Rails Guides, Active Record Transactions: https://guides.rubyonrails.org/active_record_transactions.html
- ▸Rails Guides, Action Controller Overview: https://guides.rubyonrails.org/action_controller_overview.html
- ▸RFC 7807, Problem Details for HTTP APIs: https://datatracker.ietf.org/doc/html/rfc7807
- ▸OWASP API Security Top 10: https://owasp.org/www-project-api-security/
- ▸Docker Compose Documentation: https://docs.docker.com/compose/
- ▸The Twelve-Factor App: https://12factor.net/
- ▸Sidekiq Best Practices: https://github.com/sidekiq/sidekiq/wiki/Best-Practices
- ▸RSpec Rails: https://github.com/rspec/rspec-rails
- ▸Sandi Metz, POODR: https://sandimetz.com/
✓ EOF, Israel Santos
← voltar aos artigos