You're building an AI-powered app. Your frontend is React, Vue, or a mobile client. You need Rails to serve JSON, not HTML.
Welcome to Rails API mode. Lean, fast, and exactly what you need for AI backends.
What Is API Mode?
Rails API mode strips out what you don't need (sessions, cookies, views) and keeps what you do (routing, models, JSON serialization). The result is a lighter app that boots faster and uses less memory.
Create a new API-only app:
rails new my_api --api --database=postgresql
The --api flag does the heavy lifting:
- No views or assets
- No sessions or cookies middleware
- ApplicationController inherits from ActionController::API
Your First API Endpoint
Generate a resource:
rails generate resource Post title:string body:text user:references
rails db:migrate
Update the controller for JSON:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :set_post, only: [:show, :update, :destroy]
before_action :authenticate_user!, except: [:index, :show]
def index
@posts = Post.includes(:user).all
render json: @posts
end
def show
render json: @post
end
def create
@post = current_user.posts.build(post_params)
if @post.save
render json: @post, status: :created
else
render json: { errors: @post.errors }, status: :unprocessable_entity
end
end
def update
if @post.update(post_params)
render json: @post
else
render json: { errors: @post.errors }, status: :unprocessable_entity
end
end
def destroy
@post.destroy
head :no_content
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body)
end
end
Serializers: Clean JSON
Don't expose every column. Use active_model_serializers or jsonapi-serializer (fast_jsonapi):
# Gemfile
gem 'jsonapi-serializer'
# app/serializers/post_serializer.rb
class PostSerializer
include JSONAPI::Serializer
attributes :title, :body, :created_at
belongs_to :user
end
Update your controller:
def index
@posts = Post.includes(:user).all
render json: PostSerializer.new(@posts).serializable_hash
end
Token Authentication
APIs don't use sessions. They use tokens. Here's a simple implementation:
rails generate migration AddApiTokenToUsers api_token:string
rails db:migrate
Add to your User model:
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
has_secure_token :api_token
before_create :generate_api_token
private
def generate_api_token
self.api_token = SecureRandom.hex(32)
end
end
Authentication concern:
# app/controllers/concerns/authenticable.rb
module Authenticable
extend ActiveSupport::Concern
included do
before_action :authenticate_user!
end
def authenticate_user!
token = request.headers['Authorization']&.split(' ')&.last
@current_user = User.find_by(api_token: token)
render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_user
end
def current_user
@current_user
end
end
Include it where needed:
class ApplicationController < ActionController::API
include Authenticable
end
Versioning Your API
When you change the API, don't break existing clients:
app/
controllers/
api/
v1/
posts_controller.rb
v2/
posts_controller.rb
Routes:
# config/routes.rb
namespace :api do
namespace :v1 do
resources :posts
end
namespace :v2 do
resources :posts
end
end
Testing with curl
# Create a user (in Rails console: User.create(email: "test@example.com", password: "password123"))
# Get token from that user
# List posts
curl http://localhost:3000/api/v1/posts
# Create post with token
curl -X POST http://localhost:3000/api/v1/posts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"post":{"title":"My Post","body":"Content here"}}'
CORS Configuration
Your frontend runs on a different port. Allow it:
# Gemfile
gem 'rack-cors'
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3000' # Your frontend URL
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
Rate Limiting (Optional but Smart)
APIs get hammered. Add basic rate limiting:
# Gemfile
gem 'rack-attack'
# config/initializers/rack_attack.rb
class Rack::Attack
throttle('req/ip', limit: 300, period: 5.minutes) do |req|
req.ip
end
throttle('api/ip', limit: 100, period: 1.minute) do |req|
req.ip if req.path.start_with?('/api/')
end
end
What About the Frontend?
Your Rails API doesn't care. React, Vue, Swift, Kotlin, even another Rails app using Net::HTTP. It serves JSON. That's it.
For AI features, your frontend sends requests to Rails, Rails talks to OpenAI/Claude/etc, returns the response as JSON.
Next Up: Hotwire & Turbo
API mode is great for SPAs. But what if you want real-time updates without writing JavaScript? That's where Hotwire comes in. We'll build real-time AI chat without a framework.
Series: Ruby for AI — Part 11 of 34
Top comments (0)