RoR Build a JSON API

A JSON API lets other applications talk to your Rails app programmatically. Mobile apps, JavaScript frontends, and third-party services all use APIs to fetch and send data. Rails makes building a JSON API straightforward — your controllers respond with JSON instead of HTML, and your routes stay the same.

API vs HTML Response

HTML Response (browser visit):
  GET /articles
  Returns: full HTML page with layout, nav, footer

JSON Response (API call):
  GET /articles
  Accept: application/json
  Returns: { "articles": [ { "id": 1, "title": "..." }, ... ] }

The same controller action can serve both HTML and JSON using Rails format handling.

Option 1: API-Only Rails App

Create a lightweight Rails app with no views or asset pipeline:

rails new myapi --api

This strips out middleware and modules your API does not need, making it faster and lighter.

Option 2: Add JSON Responses to an Existing App

In any controller, respond to both HTML and JSON requests:

class ArticlesController < ApplicationController
  def index
    @articles = Article.all

    respond_to do |format|
      format.html                          ← renders index.html.erb
      format.json { render json: @articles }  ← returns JSON
    end
  end

  def show
    @article = Article.find(params[:id])

    respond_to do |format|
      format.html
      format.json { render json: @article }
    end
  end
end

Test the JSON response:

curl http://localhost:3000/articles.json
curl -H "Accept: application/json" http://localhost:3000/articles

Controlling What JSON Returns

By default, render json: @article serializes all columns. Control the output with only and except:

render json: @article, only: [:id, :title, :body, :created_at]
render json: @article, except: [:updated_at, :user_id]
render json: @articles, only: [:id, :title]

Custom JSON Structure

Build a custom hash to control exactly what the response looks like:

def show
  @article = Article.find(params[:id])
  render json: {
    id:         @article.id,
    title:      @article.title,
    body:       @article.body,
    author:     @article.user.name,
    published:  @article.published?,
    created_at: @article.created_at.strftime("%Y-%m-%d")
  }
end

Full CRUD JSON API Controller

class Api::V1::ArticlesController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :set_article, only: [:show, :update, :destroy]

  def index
    articles = Article.all.order(created_at: :desc)
    render json: articles, only: [:id, :title, :body, :created_at]
  end

  def show
    render json: @article
  end

  def create
    article = Article.new(article_params)
    if article.save
      render json: article, status: :created
    else
      render json: { errors: article.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def update
    if @article.update(article_params)
      render json: @article
    else
      render json: { errors: @article.errors.full_messages }, status: :unprocessable_entity
    end
  end

  def destroy
    @article.destroy
    render json: { message: "Article deleted." }, status: :ok
  end

  private

  def set_article
    @article = Article.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render json: { error: "Article not found." }, status: :not_found
  end

  def article_params
    params.require(:article).permit(:title, :body, :published)
  end
end

API Versioning in Routes

Version your API so changes do not break existing clients:

config/routes.rb

namespace :api do
  namespace :v1 do
    resources :articles
    resources :users, only: [:index, :show]
  end

  namespace :v2 do
    resources :articles    ← new version with different response format
  end
end

This creates URLs like /api/v1/articles and /api/v2/articles. Clients on v1 keep working when you release v2.

HTTP Status Codes

CodeSymbolWhen to Use
200:okSuccessful GET, PUT, PATCH, DELETE
201:createdSuccessful POST (new record created)
204:no_contentSuccessful DELETE with no body returned
400:bad_requestMalformed request from client
401:unauthorizedAuthentication required
403:forbiddenAuthenticated but not permitted
404:not_foundRecord does not exist
422:unprocessable_entityValidation errors
500:internal_server_errorServer crashed

Token Authentication for APIs

Browser sessions do not work for API clients. Use token-based authentication instead:

rails generate migration AddApiTokenToUsers api_token:string
rails db:migrate

class User < ApplicationRecord
  before_create :generate_api_token

  private
  def generate_api_token
    self.api_token = SecureRandom.hex(24)
  end
end
class ApplicationController < ActionController::API
  def authenticate_api_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
end

API clients send the token in the Authorization header:

curl -H "Authorization: Bearer abc123yourtokenhere" http://localhost:3000/api/v1/articles

JSON Serializers

For large APIs with complex response structures, use a serializer gem like jsonapi-serializer:

gem "jsonapi-serializer"
bundle install

rails generate serializer Article id title body published created_at
app/serializers/article_serializer.rb

class ArticleSerializer
  include JSONAPI::Serializer
  attributes :id, :title, :body, :published, :created_at

  belongs_to :user
  has_many :comments
end
In controller:
render json: ArticleSerializer.new(@article).serializable_hash
render json: ArticleSerializer.new(@articles).serializable_hash

Serializers separate your JSON shape from your model, making it easy to change the API response without touching business logic.

Leave a Comment

Your email address will not be published. Required fields are marked *