← voltar aos artigos
17/07/2026Ruby on Rails · Arquitetura · API · Docker

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:

plaintext
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ínio

Uma estrutura de pastas comum para isso:

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

ruby
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
end

Controller enxuto

ruby
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
end

Serializer dedicado

ruby
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çãoComportamento comumComportamento esperado
Registro inexistente (find)500 (RecordNotFound)404 com JSON padronizado
Parâmetros inválidos ou ausentes400/500 inconsistente422 com mensagem clara
Erro de validação (save!)500422 com detalhes do erro
Erro interno inesperadostack trace exposto500 genérico + log estruturado

Centralizar o tratamento de erros no ApplicationController evita repetir a mesma lógica em cada controller:

ruby
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
end

Erros específicos do domínio também merecem uma classe própria, em vez de reaproveitar exceções genéricas do Ruby:

ruby
class OrderNotCancelableError < StandardError; end

class ApplicationController < ActionController::API
  rescue_from OrderNotCancelableError do
    render json: { error: "Order cannot be canceled" }, status: :unprocessable_entity
  end
end

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

ruby
def total
  order_items.includes(:product).sum { |item| item.price * item.quantity }
end

A segunda mantém o total denormalizado, mas com garantias de atomicidade:

ruby
ActiveRecord::Base.transaction do
  order_item.save!
  order.recalculate_total!
  order.save!
end

Sobre 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.

ruby
private

def order_params
  params.require(:order).permit(items: [:product_id, :quantity])
end
CamadaResponsabilidade
ModelInvariantes de dados (quantidade > 0, total >= 0)
ServiceRegras do caso de uso (produto existe, pedido pode ser cancelado)
ControllerFormato 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:

bash
docker compose up --build
# API disponível em http://localhost:3000
# worker de background jobs rodando

Um docker-compose completo normalmente cobre banco, cache/fila, aplicação e worker assíncrono:

yaml
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.

ÁreaCobertura comumCobertura desejável
Operação principal (sucesso)cobrecobre
Registro inexistentenão cobrecobre, 404
Parâmetros inválidosnão cobrecobre, 422
Jobs assíncronoscobertura básicacobre + edge cases
Services isolados (sem HTTP)não cobrecobre

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

EOF, Israel Santos

← voltar aos artigos