Rails.application.config.middleware.insert_before 0, Rack::Cors do
# Development CORS - allow any localhost
allow do
origins(
/http:\/\/localhost:\d+/,
/http:\/\/127\.0\.0\.1:\d+/,
'http://localhost:5173', # Vite default
'http://localhost:3001' # Alternative React dev server
)
resource '/api/*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true,
expose: ['Authorization', 'X-Request-ID']
end if Rails.env.development?
# Production CORS - strict origin checking
allow do
origins ENV.fetch('ALLOWED_ORIGINS', '').split(',')
resource '/api/*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true,
expose: ['Authorization', 'X-Request-ID'],
max_age: 86400 # Cache preflight for 24 hours
end if Rails.env.production?
# Public endpoints - no credentials
allow do
origins '*'
resource '/api/public/*',
headers: :any,
methods: [:get, :options, :head],
credentials: false
end
end
Cross-Origin Resource Sharing (CORS) allows browsers to make requests from React apps hosted on different domains than the Rails API. The rack-cors gem configures CORS middleware with fine-grained control over origins, methods, and headers. In development, I allow localhost origins with various ports for flexibility. Production restricts origins to specific domains. The credentials: true option enables cookies and authentication headers. expose headers make custom headers like Authorization accessible to JavaScript. Preflight OPTIONS requests happen automatically for complex requests. Wildcard origins (*) work for public APIs but disable credentials. Proper CORS configuration is essential for SPA architectures.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.