DEV Community

AgentQ
AgentQ

Posted on

Rails API Mode — Building JSON APIs for AI-Powered Apps

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Serializers: Clean JSON

Don't expose every column. Use active_model_serializers or jsonapi-serializer (fast_jsonapi):

# Gemfile
gem 'jsonapi-serializer'
Enter fullscreen mode Exit fullscreen mode
# app/serializers/post_serializer.rb
class PostSerializer
  include JSONAPI::Serializer

  attributes :title, :body, :created_at
  belongs_to :user
end
Enter fullscreen mode Exit fullscreen mode

Update your controller:

def index
  @posts = Post.includes(:user).all
  render json: PostSerializer.new(@posts).serializable_hash
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Include it where needed:

class ApplicationController < ActionController::API
  include Authenticable
end
Enter fullscreen mode Exit fullscreen mode

Versioning Your API

When you change the API, don't break existing clients:

app/
  controllers/
    api/
      v1/
        posts_controller.rb
      v2/
        posts_controller.rb
Enter fullscreen mode Exit fullscreen mode

Routes:

# config/routes.rb
namespace :api do
  namespace :v1 do
    resources :posts
  end

  namespace :v2 do
    resources :posts
  end
end
Enter fullscreen mode Exit fullscreen mode

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"}}'
Enter fullscreen mode Exit fullscreen mode

CORS Configuration

Your frontend runs on a different port. Allow it:

# Gemfile
gem 'rack-cors'
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

Rate Limiting (Optional but Smart)

APIs get hammered. Add basic rate limiting:

# Gemfile
gem 'rack-attack'
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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)