Claude API Integration with a Rails App: Full Guide
If you're building a Rails app and want to add Claude for chat, summarization, or content generation, the integration itself is straightforward: you make HTTPS requests to an API endpoint with a JSON body, handle the JSON (or streamed) response, and wire that into your controllers, jobs, or Turbo Streams. There's no official Claude gem, so most Rails developers either use Net::HTTP/Faraday directly or go through a proxy service that gives them a stable, key-based API.
This guide walks through both approaches: calling Claude directly from Rails, and using a lightweight service layer so your controllers stay clean. It also covers streaming responses into Turbo Streams, background job patterns for longer requests, and common pitfalls (timeouts, rate limits, request/response shape).
Setting Up the Basics
Whichever HTTP client you use, you need three things: an API key stored as a credential, a model name, and a service object that isolates the API call from your controllers.
Add your key to Rails credentials instead of .env files when possible:
EDITOR="code --wait" bin/rails credentials:edit
claude:
api_key: sk-ant-xxxxxxxx
Then build a service object:
# app/services/claude_client.rb
class ClaudeClient
include HTTParty
base_uri "https://api.anthropic.com/v1"
def initialize
@headers = {
"x-api-key" => Rails.application.credentials.dig(:claude, :api_key),
"anthropic-version" => "2023-06-01",
"content-type" => "application/json"
}
end
def send_message(prompt, model: "claude-sonnet-4-5")
self.class.post(
"/messages",
headers: @headers,
body: {
model: model,
max_tokens: 1024,
messages: [{ role: "user", content: prompt }]
}.to_json
)
end
end
Call it from a controller:
class ChatsController < ApplicationController
def create
response = ClaudeClient.new.send_message(params[:prompt])
@reply = response.parsed_response.dig("content", 0, "text")
render turbo_stream: turbo_stream.append("chat", partial: "chats/message", locals: { text: @reply })
end
end
This works, but a few things become painful as your app grows: managing API key rotation, tracking per-user usage, handling rate limits gracefully, and giving each part of your app (or each customer, if you're multi-tenant) its own scoped key.
Using a Managed API Layer Instead
If you don't want to manage raw API keys, rate limit handling, and usage tracking yourself, you can put SubToAPI in front of Claude. It converts your existing Claude access into a standard HTTPS API with its own sub_live_... application keys, so your Rails app talks to one stable endpoint regardless of how many team members or environments you have.
The Rails-side code barely changes — you're still POSTing JSON to a /messages-style endpoint:
# app/services/claude_client.rb
class ClaudeClient
include HTTParty
base_uri "https://api.subtoapi.app/v1"
def initialize
@headers = {
"Authorization" => "Bearer #{Rails.application.credentials.dig(:subtoapi, :key)}",
"content-type" => "application/json"
}
end
def send_message(prompt, model: "claude-sonnet-4-5")
self.class.post(
"/messages",
headers: @headers,
body: {
model: model,
max_tokens: 1024,
messages: [{ role: "user", content: prompt }]
}.to_json
)
end
end
What you get in return: per-application keys you can issue for staging, production, or individual team members without ever exposing the underlying Claude account, a dashboard for usage metadata across the whole team, and team seats so multiple developers can build against the same Claude access without sharing raw credentials. Setup takes a few minutes via /signup, and the request/response format matches what's documented at /docs/messages, so you're not learning a new API shape — just pointing your existing HTTP client at a different base URL.
Streaming Into Turbo Streams
For chat-style UIs, streaming tokens as they arrive gives a much better UX than waiting for the full response. Rails' ActionController::Live combined with Turbo Streams handles this well:
class ChatsController < ApplicationController
include ActionController::Live
def stream
response.headers["Content-Type"] = "text/event-stream"
uri = URI("https://api.subtoapi.app/v1/messages")
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{Rails.application.credentials.dig(:subtoapi, :key)}"
request["content-type"] = "application/json"
request.body = {
model: "claude-sonnet-4-5",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: params[:prompt] }]
}.to_json
http.request(request) do |api_response|
api_response.read_body do |chunk|
response.stream.write(chunk)
end
end
end
ensure
response.stream.close
end
end
On the frontend, parse the server-sent events and append text chunks to a Turbo Stream target as they arrive. See /docs/streaming for the exact event format and chunk structure.
Background Jobs for Longer Requests
Not every use case needs streaming. For summarization, report generation, or batch content tasks, running the request in a Sidekiq job avoids tying up a web worker:
class ClaudeGenerationJob < ApplicationJob
queue_as :default
def perform(record_id, prompt)
record = Document.find(record_id)
response = ClaudeClient.new.send_message(prompt)
text = response.parsed_response.dig("content", 0, "text")
record.update!(generated_content: text)
Turbo::StreamsChannel.broadcast_replace_to(
record, target: "document_#{record.id}", partial: "documents/content", locals: { document: record }
)
end
end
This pattern keeps controllers thin and gives you natural retry semantics through your job queue if a request times out or hits a rate limit.
Handling Tool Use From Rails
If your app needs Claude to call functions — looking up a record, running a calculation, querying an internal API — you define tools in the request body and handle the tool_use block in the response:
tools = [{
name: "lookup_order",
description: "Look up an order by ID",
input_schema: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"]
}
}]
Your Rails app inspects the response, executes the matching Ruby method, and sends the result back in a follow-up message. The full request/response cycle for this pattern is documented at /docs/tools.
Common Pitfalls
- Timeouts: Long generations can exceed default HTTP client timeouts. Set explicit timeouts (
HTTParty.post(..., timeout: 60)) rather than relying on defaults. - Rate limits: Handle 429 responses with exponential backoff, especially under concurrent Puma workers.
- Blocking the request cycle: Avoid calling Claude synchronously inside a request that also needs to respond quickly to the user — use a job or streaming instead.
- Key sprawl: If multiple environments or team members need access, avoid passing around one shared secret key — scope keys per use case where possible.
Questions
Do I need a Ruby gem to call the Claude API from Rails? No. There's no official gem, but any HTTP client (Faraday, HTTParty, Net::HTTP) works fine since it's a standard JSON-over-HTTPS API.
How do I stream Claude responses into a Rails view? Use ActionController::Live to proxy the server-sent event stream, then append incoming text chunks to a Turbo Stream target on the frontend as they arrive.
Should API calls run in the request cycle or a background job? For anything longer than a second or two, use a Sidekiq job or streaming response — synchronous calls inside a normal request risk timeouts and block the web worker. See /docs/quickstart for request patterns that work either way.