← Blog

Claude API Ruby on Rails Example: Full Integration

2026-10-18 · 4 min read · SubToAPI Team

If you're searching for a Claude API Ruby on Rails example, you're probably trying to add AI features to an existing Rails app — a support assistant, content generator, or internal tool — without wrestling with raw HTTP calls scattered across your controllers. This article walks through a complete, production-shaped setup: a service object, a controller endpoint, background job handling for long requests, and streaming responses back to the browser.

Rails doesn't have an official Claude SDK, but that's fine. The API is plain HTTPS with JSON, and Ruby's Net::HTTP or the faraday gem handles it cleanly. The pattern below works whether you're calling Claude directly or through a proxy like SubToAPI that gives you an application API key instead of managing raw credentials per environment.

Setting Up the Gemfile

You only need an HTTP client. faraday is the most common choice in Rails apps:

# Gemfile
gem 'faraday'
gem 'faraday-retry'

Run bundle install, then store your key in Rails credentials or an environment variable — never commit it:

rails credentials:edit
claude:
  api_key: sk-ant-xxxxxxxx

Building a Service Object

Keeping the API logic out of controllers makes it testable and reusable. Create app/services/claude_client.rb:

class ClaudeClient
  BASE_URL = "https://api.anthropic.com/v1/messages"

  def initialize(api_key: Rails.application.credentials.dig(:claude, :api_key))
    @api_key = api_key
    @conn = Faraday.new(url: BASE_URL) do |f|
      f.request :json
      f.response :json
      f.request :retry, max: 2, interval: 0.5
      f.adapter Faraday.default_adapter
    end
  end

  def complete(prompt, model: "claude-sonnet-4-5", max_tokens: 1024)
    response = @conn.post do |req|
      req.headers["x-api-key"] = @api_key
      req.headers["anthropic-version"] = "2023-06-01"
      req.headers["content-type"] = "application/json"
      req.body = {
        model: model,
        max_tokens: max_tokens,
        messages: [{ role: "user", content: prompt }]
      }
    end

    raise "Claude API error: #{response.status}" unless response.success?

    response.body.dig("content", 0, "text")
  end
end

This gives you a single, testable method: ClaudeClient.new.complete("Summarize this ticket").

Wiring It Into a Controller

A typical use case is generating a response for a model instance — say, drafting a reply to a support ticket:

class TicketRepliesController < ApplicationController
  def create
    ticket = Ticket.find(params[:ticket_id])
    draft = ClaudeClient.new.complete(
      "Draft a polite support reply for: #{ticket.body}"
    )

    render json: { draft: draft }
  rescue => e
    render json: { error: e.message }, status: :bad_gateway
  end
end

Keep the controller thin. Validation, retries, and prompt construction belong in the service layer or a dedicated builder object if prompts get complex.

Handling Long-Running Requests with ActiveJob

Claude calls can take several seconds, which is too long for a synchronous request-response cycle in most Rails apps. Move it to a background job and notify the client via a webhook, polling endpoint, or ActionCable:

class GenerateReplyJob < ApplicationJob
  queue_as :default

  def perform(ticket_id)
    ticket = Ticket.find(ticket_id)
    draft = ClaudeClient.new.complete("Draft a reply for: #{ticket.body}")

    ticket.update!(ai_draft: draft)
    TicketChannel.broadcast_to(ticket, { draft: draft })
  end
end

Trigger it from the controller with GenerateReplyJob.perform_later(ticket.id) and return a 202 immediately. This keeps your Rails workers from blocking on a slow upstream call — a common gotcha when teams first wire Claude into Sidekiq-backed apps.

Streaming Responses

For chat-style UIs, streaming tokens as they arrive feels far more responsive than waiting for the full response. Claude supports server-sent events natively, and Rails can relay them using ActionController::Live:

class StreamController < ApplicationController
  include ActionController::Live

  def show
    response.headers["Content-Type"] = "text/event-stream"
    conn = Faraday.new(url: "https://api.anthropic.com/v1/messages")

    conn.post do |req|
      req.headers["x-api-key"] = Rails.application.credentials.dig(:claude, :api_key)
      req.headers["anthropic-version"] = "2023-06-01"
      req.body = {
        model: "claude-sonnet-4-5",
        max_tokens: 1024,
        stream: true,
        messages: [{ role: "user", content: params[:prompt] }]
      }.to_json
      req.options.on_data = Proc.new do |chunk, _|
        response.stream.write(chunk)
      end
    end
  ensure
    response.stream.close
  end
end

This is where a lot of teams hit friction — raw SSE parsing, reconnect logic, and partial JSON chunks add real complexity. If you'd rather skip building that plumbing, SubToAPI exposes the same streaming behavior through a standard sub_live_ key with a simpler client contract; see /docs/streaming for the request format.

Simplifying the Setup with SubToAPI

The service object pattern above works regardless of which key you use. If you're managing multiple environments (staging, CI, per-developer sandboxes) and don't want to pass around a single shared Claude credential, SubToAPI lets you issue a scoped sub_live_ key per environment or team member, with usage visible per key in one dashboard. Swapping it into the ClaudeClient class above is a one-line change — point BASE_URL at https://api.subtoapi.app/v1/messages and use your SUBTOAPI_KEY. Check /docs/quickstart for the exact request shape and /pricing for plan details (Solo, Team, Scale).

Testing the Integration

Stub the HTTP layer in RSpec rather than hitting the real API in CI:

RSpec.describe ClaudeClient do
  it "returns the completion text" do
    stub_request(:post, ClaudeClient::BASE_URL)
      .to_return(
        status: 200,
        body: { content: [{ text: "Hello" }] }.to_json,
        headers: { "Content-Type" => "application/json" }
      )

    expect(ClaudeClient.new.complete("Hi")).to eq("Hello")
  end
end

webmock keeps your test suite fast and avoids burning real API quota on every CI run.

questions

Does Rails need a dedicated Claude gem? No. The API is standard JSON over HTTPS, so faraday or Net::HTTP is sufficient. A thin service object keeps the integration organized without extra dependencies.

How do I avoid blocking web requests when calling Claude from Rails? Move the call into an ActiveJob (Sidekiq, GoodJob, etc.) and notify the client via ActionCable, polling, or a webhook once the job completes, rather than waiting synchronously in the controller.

Can I stream Claude responses to a Rails frontend? Yes, using ActionController::Live with Content-Type: text/event-stream, relaying chunks as they arrive from Claude's streaming endpoint. See /docs/streaming for details on handling SSE chunks correctly.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →