Helpers de Rails que uso a diario

Ingenieria 06/02/2026

Ruby on Rails es mi framework principal desde hace años, y proyecto tras proyecto me descubría reescribiendo los mismos patrones de memoria. Rails Notes es mi respuesta: una biblioteca de componentes probados en producción (servicios, jobs, Mongoid, Kafka) lista para llevar al siguiente proyecto.

Categoría:

Ingenieria

Tecnologías:

Rails 7, Ruby 3.4, MongoDB, Mongoid, Kafka, Sidekiq, Redis, RSpec, Docker

Resumen del proyecto

Por qué Rails

Ruby on Rails es mi framework principal desde hace muchos años, con él he construido desde plataformas de coleccionables deportivos hasta backends de alto tráfico. La razón es aburrida y por eso buena: sus convenciones deciden por ti las mil pequeñas cosas que no importan, y a un equipo pequeño (o de uno) eso le regala una productividad que pocos frameworks igualan. Pero ningún framework te libra de tus propias repeticiones: de eso va esta biblioteca.

El problema

Cuando programas muchos proyectos con el mismo framework, te pasa algo curioso: no reescribes lo difícil, reescribes lo de siempre. El servicio con su patrón de llamada, el enum que Mongoid no trae, la espera de un lote de jobs de Sidekiq. Y reescribir de memoria tiene un coste escondido: cada proyecto acaba con su versión ligeramente distinta del mismo patrón, con sus bugs ligeramente distintos.

La idea

Un repositorio-biblioteca: componentes reales, extraídos de aplicaciones en producción, con sus tests y su documentación. No es una aplicación, es la caja de piezas de la que arranca cada proyecto nuevo. Tres ejemplos:

1. Servicios con un solo verbo

Todos mis servicios se llaman igual: call. Este pequeño concern impone el patrón: el servicio se instancia y ejecuta en una línea, y si a alguien se le olvida implementar call, explota en desarrollo, no en producción:

module Callable
  extend ActiveSupport::Concern

  included do
    def call
      raise NotImplementedError
    end
  end

  module ClassMethods
    def call(*args)
      new(*args).call
    end
  end
end
# Uso: da igual el servicio, siempre la misma forma
InvoiceExporter.call(invoice)

Parece poca cosa. El valor está en lo que evita: veinte servicios con veinte formas de llamarse.

2. El enum que Mongoid no trae

ActiveRecord tiene enum; Mongoid, no. En vez de simularlo distinto en cada modelo, un concern que genera constantes, accessors y métodos de conveniencia a partir de una declaración:

class Card
  include Mongoid::Document
  include FieldEnumerable

  field :source, type: Integer
  field_enum source: { marketplace: 0, battle_arena: 1 }, _prefix: true
end

# Y a partir de ahí, como en ActiveRecord:
card.source_marketplace?   # => true/false
Card::SOURCE_MARKETPLACE   # => 0

3. Esperar a un lote de jobs sin inventos

Lanzar cien jobs de Sidekiq es fácil; saber cuándo han terminado todos, no tanto. Un pool que registra los IDs y espera:

pool = AsyncJobPool.new
cards.each { |card| pool.add(CardRenderJob.perform_async(card.id)) }
pool.wait_for_all   # bloquea hasta que el lote termina (o reporta los fallidos)

4. Agregaciones de MongoDB que se pueden leer

Las agregaciones de MongoDB en crudo son arrays de hashes que crecen hasta volverse ilegibles. Aggregator las convierte en una cadena de etapas componible:

# Antes: el hash crudo
Card.collection.aggregate([
  { "$match" => { "season" => season_id } },
  { "$group" => { "_id" => "$player_id", "total" => { "$sum" => 1 } } },
  { "$sort"  => { "total" => -1 } }
])

# Después: etapas encadenadas, con helpers para las expresiones
Aggregate::Aggregator.new(Card)
  .add_stage(:match, { season: season_id })
  .add_stage(:group, { _id: "$player_id", total: { "$sum": 1 } })
  .add_stage(:sort,  { total: -1 })
  .apply

El $ de cada etapa se normaliza solo, las expresiones comunes (in, eq, lt…) tienen helper, y el pipeline se puede construir por partes (un filtro aquí, una etapa condicional allá) antes de ejecutarlo.

Más piezas de la caja

Por dar una idea de lo que se acumula con los años:

  • Aggregate: pipelines de agregación de MongoDB componibles por etapas, en vez de arrays de hashes ilegibles.
  • BaseSearch: filtros, ordenación y paginación de APIs con un patrón único para todos los listados.
  • KafkaIntegration: productores y consumidores (Racecar/DeliveryBoy) con configuración de brokers por entorno.
  • ChainedJobPool: el hermano de AsyncJobPool: jobs en secuencia, cada uno cuando termina el anterior.
  • CallbacksLoader: callbacks de modelos en módulos propios, fuera del modelo, y cableados solos.
  • Middleware de autenticación: Bearer tokens verificados en la puerta, antes de llegar al controlador.
  • MongoidTransaction / enhancements: transacciones seguras y consultas read-only en Mongoid.
  • InternalApi / CloudflareApi: adaptadores HTTP con manejo de errores para servicios internos y para bloquear IPs en Cloudflare.

Cómo aterriza en un proyecto nuevo

Sin ceremonia: o entra la gema con la caja completa, o se copia la pieza suelta al proyecto. Cada componente lleva sus specs, que hacen de documentación ejecutable: si dudas de cómo se usa BaseSearch, lees su test y ves un ejemplo funcionando. Esa es otra regla de la biblioteca: pieza sin spec no entra.

Un detalle de ingeniería honesto

Estos componentes viven también empaquetados en una gema propia que agrupa la caja de herramientas completa. Pero (honestidad) hay proyectos donde no monto la gema entera: copio la pieza suelta que necesito y listo. Cuarenta líneas que entiendes a veces valen más que una dependencia; la gema compensa cuando vas a usar media caja. Tener las dos opciones es justo el objetivo de esta biblioteca.

El resultado

Cada proyecto Rails nuevo arranca con las piezas aburridas ya resueltas y probadas. Es la diferencia entre empezar desde cero y empezar con ventaja, y la razón de que dos proyectos míos separados por años se lean como escritos por la misma mano. Porque lo están.