Claude Code for Ruby: Rack Middleware, Rails Patterns, and Background Jobs — Claude Skills 360 Blog
Blog / Backend / Claude Code for Ruby: Rack Middleware, Rails Patterns, and Background Jobs
Backend

Claude Code for Ruby: Rack Middleware, Rails Patterns, and Background Jobs

Published: October 8, 2026
Read time: 8 min read
By: Claude Skills 360

Ruby’s Rack middleware model and Rails conventions make for productive web development, but the patterns that scale — service objects, form objects, background jobs with Sidekiq — require deliberate architecture. Claude Code writes idiomatic Ruby, understands Rails conventions deeply, and generates the Rack middleware, service objects, and Sidekiq worker patterns that keep large Rails codebases maintainable.

CLAUDE.md for Ruby/Rails Projects

## Stack
- Ruby 3.3, Rails 7.1
- Database: PostgreSQL via ActiveRecord
- Background jobs: Sidekiq 7 (Redis-backed)
- Testing: RSpec + FactoryBot + Shoulda-matchers
- Code style: Standard Ruby (rubocop-performance, no rubocop-rails)
- Service objects: app/services/, always return a Result object
- No fat models — business logic goes in service objects
- ActiveRecord scopes for reusable query building; no scope chains in controllers
- N+1 queries are bugs — use includes/preload/eager_load

Rack Middleware

# lib/middleware/request_id_middleware.rb
# Every request gets a unique ID — propagates to logs and downstream services
class RequestIdMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    request_id = env['HTTP_X_REQUEST_ID'] || SecureRandom.uuid
    
    # Store in thread-local for access throughout request lifecycle
    RequestContext.request_id = request_id
    
    status, headers, body = @app.call(env)
    
    headers['X-Request-Id'] = request_id
    [status, headers, body]
  ensure
    RequestContext.clear
  end
end

# config/application.rb
config.middleware.insert_before ActionDispatch::RequestId, RequestIdMiddleware
# lib/middleware/api_rate_limiter.rb — Rack-level rate limiting
class ApiRateLimiter
  LIMIT = 100     # requests
  WINDOW = 60     # seconds

  def initialize(app, redis:)
    @app = app
    @redis = redis
  end

  def call(env)
    request = Rack::Request.new(env)
    return @app.call(env) unless api_request?(request)

    identifier = request.env['HTTP_X_API_KEY'] || request.ip
    key = "rate_limit:#{identifier}"
    
    count = @redis.incr(key)
    @redis.expire(key, WINDOW) if count == 1

    if count > LIMIT
      retry_after = @redis.ttl(key)
      return [
        429,
        {
          'Content-Type' => 'application/json',
          'Retry-After' => retry_after.to_s,
          'X-RateLimit-Limit' => LIMIT.to_s,
          'X-RateLimit-Remaining' => '0',
        },
        ['{"error":"Rate limit exceeded"}'],
      ]
    end

    status, headers, body = @app.call(env)
    headers['X-RateLimit-Limit'] = LIMIT.to_s
    headers['X-RateLimit-Remaining'] = [LIMIT - count, 0].max.to_s
    [status, headers, body]
  end

  private

  def api_request?(request)
    request.path.start_with?('/api/')
  end
end

Service Objects

# app/services/place_order_service.rb
# Service objects: single public method, return a Result
class PlaceOrderService
  Result = Data.define(:success, :order, :errors)

  def initialize(customer:, cart:, payment_method_id:)
    @customer = customer
    @cart = cart
    @payment_method_id = payment_method_id
  end

  def call
    return Result.new(success: false, order: nil, errors: ['Cart is empty']) if @cart.items.empty?
    return Result.new(success: false, order: nil, errors: ['Account suspended']) if @customer.suspended?

    order = nil

    ActiveRecord::Base.transaction do
      order = Order.create!(
        customer: @customer,
        total_cents: @cart.total_cents,
        status: 'pending',
      )

      @cart.items.each do |item|
        order.line_items.create!(
          product: item.product,
          quantity: item.quantity,
          unit_price_cents: item.product.price_cents,
        )
      end

      # Charge card
      charge = StripeChargeService.new(
        customer_id: @customer.stripe_id,
        amount_cents: order.total_cents,
        payment_method_id: @payment_method_id,
        idempotency_key: "order-#{order.id}",
      ).call

      raise ActiveRecord::Rollback unless charge.success

      order.update!(stripe_charge_id: charge.charge_id, status: 'paid')
    end

    OrderConfirmationMailer.with(order: order).deliver_later

    Result.new(success: true, order: order, errors: [])
  rescue => e
    Rails.logger.error "PlaceOrderService failed: #{e.message}"
    Result.new(success: false, order: nil, errors: [e.message])
  end
end

# Controller — thin, delegates to service
class OrdersController < ApplicationController
  def create
    result = PlaceOrderService.new(
      customer: current_user,
      cart: current_cart,
      payment_method_id: params[:payment_method_id],
    ).call

    if result.success
      render json: OrderSerializer.new(result.order).serializable_hash, status: :created
    else
      render json: { errors: result.errors }, status: :unprocessable_entity
    end
  end
end

ActiveRecord: N+1 Prevention

# Bad: N+1 — loads 1 query for orders, then N queries for customers
orders = Order.where(status: 'pending').limit(50)
orders.each { |o| puts o.customer.email }  # 51 queries

# Good: eager load with includes
orders = Order.includes(:customer, line_items: :product).where(status: 'pending').limit(50)
orders.each { |o| puts o.customer.email }  # 3 queries

# When you need WHERE on the association: use joins + references
orders = Order.joins(:customer)
              .where(customers: { plan: 'enterprise' })
              .includes(:line_items)
              .order('orders.created_at DESC')
              .limit(50)

# Scope for reusable query building
class Order < ApplicationRecord
  scope :pending, -> { where(status: 'pending') }
  scope :recent, -> { order(created_at: :desc) }
  scope :with_details, -> { includes(:customer, line_items: :product) }
  
  # Counter cache: avoid COUNT queries
  belongs_to :customer, counter_cache: true
end

# Bulk operations: avoid model callbacks for large updates
# Bad: loads all records into memory
Order.pending.each { |o| o.update!(status: 'expired') }

# Good: single UPDATE statement
Order.pending.where('created_at < ?', 24.hours.ago).update_all(status: 'expired')

Sidekiq Workers

# app/workers/send_order_confirmation_worker.rb
class SendOrderConfirmationWorker
  include Sidekiq::Job

  sidekiq_options(
    queue: 'emails',
    retry: 5,           # Retry up to 5 times with exponential backoff
    backtrace: true,
  )

  def perform(order_id)
    order = Order.includes(:customer, :line_items).find(order_id)
    OrderConfirmationMailer.with(order: order).confirmation_email.deliver_now
  rescue ActiveRecord::RecordNotFound => e
    # Don't retry — the order was deleted
    Sidekiq.logger.warn "Order #{order_id} not found, skipping confirmation email"
  end
end

# Periodic jobs with sidekiq-cron
# config/initializers/sidekiq_cron.rb
Sidekiq::Cron::Job.load_from_hash({
  'expire_pending_orders' => {
    cron: '*/5 * * * *',   # Every 5 minutes
    class: 'ExpirePendingOrdersWorker',
  },
  'daily_revenue_report' => {
    cron: '0 8 * * *',     # 8am daily
    class: 'DailyRevenueReportWorker',
  },
})
# app/workers/expire_pending_orders_worker.rb
class ExpirePendingOrdersWorker
  include Sidekiq::Job
  sidekiq_options queue: 'default', retry: 3

  def perform
    # Batch processing — don't load all at once
    Order.pending
         .where('created_at < ?', 30.minutes.ago)
         .in_batches(of: 500) do |batch|
      batch.each do |order|
        ExpireOrderService.new(order).call
      end
    end
  end
end

RSpec Testing

# spec/services/place_order_service_spec.rb
RSpec.describe PlaceOrderService do
  subject(:service) { described_class.new(customer:, cart:, payment_method_id: 'pm_test') }

  let(:customer) { create(:customer, :active) }
  let(:cart) { create(:cart, :with_items, customer:) }

  describe '#call' do
    context 'with valid inputs' do
      before do
        allow(StripeChargeService).to receive(:new).and_return(
          instance_double(StripeChargeService, call: double(success: true, charge_id: 'ch_123'))
        )
      end

      it 'creates an order with correct total' do
        result = service.call
        expect(result.success).to be true
        expect(result.order.total_cents).to eq(cart.total_cents)
      end

      it 'enqueues a confirmation email' do
        expect { service.call }.to change(ActionMailer::Base.deliveries, :count).by(1)
      end
    end

    context 'when payment fails' do
      before do
        allow(StripeChargeService).to receive(:new).and_return(
          instance_double(StripeChargeService, call: double(success: false))
        )
      end

      it 'does not create an order' do
        expect { service.call }.not_to change(Order, :count)
      end

      it 'returns failure result' do
        result = service.call
        expect(result.success).to be false
      end
    end

    context 'with empty cart' do
      let(:cart) { create(:cart, customer:) }  # No items

      it 'returns error without hitting Stripe' do
        result = service.call
        expect(result.errors).to include('Cart is empty')
        expect(StripeChargeService).not_to have_received(:new)
      end
    end
  end
end

For the Kafka event publishing that Rails applications can integrate with, see the Kafka guide. For the authentication patterns that protect Rails APIs, the OAuth2 guide covers session management applicable to Rails Devise configurations. The Claude Skills 360 bundle includes Ruby skill sets covering Rack middleware, service objects, Sidekiq workers, and RSpec patterns. Start with the free tier to try Rails architecture generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free