Skip
Arish's avatar

48. Building APIs with Rails


Building APIs with Rails

Rails is excellent for building RESTful APIs. You can create a full API or add API endpoints to an existing application.

API-Only Applications

Create a new API-only Rails app:

bash
1rails new my_api --api

This creates a lighter application without views, helpers, and assets.

API Controllers

Basic JSON Response

ruby
1# app/controllers/api/v1/articles_controller.rb
2module Api
3  module V1
4    class ArticlesController < ApplicationController
5      def index
6        @articles = Article.all
7        render json: @articles
8      end
9      
10      def show
11        @article = Article.find(params[:id])
12        render json: @article
13      end
14      
15      def create
16        @article = Article.new(article_params)
17        
18        if @article.save
19          render json: @article, status: :created
20        else
21          render json: { errors: @article.errors }, status: :unprocessable_entity
22        end
23      end
24      
25      def update
26        @article = Article.find(params[:id])
27        
28        if @article.update(article_params)
29          render json: @article
30        else
31          render json: { errors: @article.errors }, status: :unprocessable_entity
32        end
33      end
34      
35      def destroy
36        @article = Article.find(params[:id])
37        @article.destroy
38        head :no_content
39      end
40      
41      private
42      
43      def article_params
44        params.require(:article).permit(:title, :body, :published)
45      end
46    end
47  end
48end

Routes

ruby
1# config/routes.rb
2Rails.application.routes.draw do
3  namespace :api do
4    namespace :v1 do
5      resources :articles
6      resources :users, only: [:index, :show]
7    end
8  end
9end

API Base Controller

ruby
1# app/controllers/api/v1/base_controller.rb
2module Api
3  module V1
4    class BaseController < ApplicationController
5      # Skip CSRF for API
6      skip_before_action :verify_authenticity_token
7      
8      # Rescue from common errors
9      rescue_from ActiveRecord::RecordNotFound, with: :not_found
10      rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
11      rescue_from ActionController::ParameterMissing, with: :bad_request
12      
13      private
14      
15      def not_found(exception)
16        render json: { error: exception.message }, status: :not_found
17      end
18      
19      def unprocessable_entity(exception)
20        render json: { errors: exception.record.errors }, status: :unprocessable_entity
21      end
22      
23      def bad_request(exception)
24        render json: { error: exception.message }, status: :bad_request
25      end
26    end
27  end
28end
29
30# Inherit in other controllers
31class Api::V1::ArticlesController < Api::V1::BaseController
32  # ...
33end

JSON Serialization

Using as_json

ruby
1class Article < ApplicationRecord
2  def as_json(options = {})
3    super(options.merge(
4      only: [:id, :title, :body, :published, :created_at],
5      include: { user: { only: [:id, :name] } },
6      methods: [:reading_time]
7    ))
8  end
9end

Using Jbuilder

ruby
1# app/views/api/v1/articles/show.json.jbuilder
2json.id @article.id
3json.title @article.title
4json.body @article.body
5json.published @article.published
6json.created_at @article.created_at
7
8json.author do
9  json.id @article.user.id
10  json.name @article.user.name
11end
12
13json.tags @article.tags do |tag|
14  json.id tag.id
15  json.name tag.name
16end
ruby
1# app/views/api/v1/articles/index.json.jbuilder
2json.articles @articles do |article|
3  json.id article.id
4  json.title article.title
5  json.published article.published
6end
7
8json.meta do
9  json.total @articles.total_count
10  json.page @articles.current_page
11  json.per_page @articles.limit_value
12end

Using Active Model Serializers

ruby
1# Gemfile
2gem 'active_model_serializers'
3
4# app/serializers/article_serializer.rb
5class ArticleSerializer < ActiveModel::Serializer
6  attributes :id, :title, :body, :published, :created_at
7  
8  belongs_to :user
9  has_many :tags
10  
11  attribute :reading_time do
12    object.reading_time
13  end
14end
15
16# Controller
17def show
18  @article = Article.find(params[:id])
19  render json: @article
20end

API Authentication

Token-Based Authentication

ruby
1# Migration
2rails generate migration AddApiTokenToUsers api_token:string:uniq
3
4# app/models/user.rb
5class User < ApplicationRecord
6  has_secure_token :api_token
7end
8
9# app/controllers/api/v1/base_controller.rb
10class Api::V1::BaseController < ApplicationController
11  before_action :authenticate_api_user!
12  
13  private
14  
15  def authenticate_api_user!
16    token = request.headers['Authorization']&.split(' ')&.last
17    @current_user = User.find_by(api_token: token)
18    
19    unless @current_user
20      render json: { error: 'Unauthorized' }, status: :unauthorized
21    end
22  end
23  
24  def current_user
25    @current_user
26  end
27end

JWT Authentication

ruby
1# Gemfile
2gem 'jwt'
3
4# app/lib/json_web_token.rb
5class JsonWebToken
6  SECRET_KEY = Rails.application.secrets.secret_key_base
7  
8  def self.encode(payload, exp = 24.hours.from_now)
9    payload[:exp] = exp.to_i
10    JWT.encode(payload, SECRET_KEY)
11  end
12  
13  def self.decode(token)
14    decoded = JWT.decode(token, SECRET_KEY)[0]
15    HashWithIndifferentAccess.new(decoded)
16  rescue JWT::DecodeError
17    nil
18  end
19end
20
21# app/controllers/api/v1/authentication_controller.rb
22class Api::V1::AuthenticationController < Api::V1::BaseController
23  skip_before_action :authenticate_api_user!
24  
25  def login
26    user = User.find_by(email: params[:email])
27    
28    if user&.authenticate(params[:password])
29      token = JsonWebToken.encode(user_id: user.id)
30      render json: { token: token, exp: 24.hours.from_now }
31    else
32      render json: { error: 'Invalid credentials' }, status: :unauthorized
33    end
34  end
35end
36
37# Authenticate with JWT
38def authenticate_api_user!
39  header = request.headers['Authorization']
40  token = header.split(' ').last if header
41  decoded = JsonWebToken.decode(token)
42  
43  if decoded
44    @current_user = User.find(decoded[:user_id])
45  else
46    render json: { error: 'Unauthorized' }, status: :unauthorized
47  end
48end

CORS Configuration

ruby
1# Gemfile
2gem 'rack-cors'
3
4# config/initializers/cors.rb
5Rails.application.config.middleware.insert_before 0, Rack::Cors do
6  allow do
7    origins '*'
8    
9    resource '*',
10      headers: :any,
11      methods: [:get, :post, :put, :patch, :delete, :options, :head],
12      expose: ['Authorization']
13  end
14end

Rate Limiting

ruby
1# Gemfile
2gem 'rack-attack'
3
4# config/initializers/rack_attack.rb
5class Rack::Attack
6  throttle('api/ip', limit: 100, period: 1.minute) do |req|
7    req.ip if req.path.start_with?('/api')
8  end
9  
10  throttle('api/token', limit: 1000, period: 1.hour) do |req|
11    req.env['HTTP_AUTHORIZATION'] if req.path.start_with?('/api')
12  end
13end

Pagination

ruby
1# Gemfile
2gem 'kaminari'
3
4# Controller
5def index
6  @articles = Article.page(params[:page]).per(params[:per_page] || 25)
7  
8  render json: {
9    articles: @articles,
10    meta: {
11      current_page: @articles.current_page,
12      total_pages: @articles.total_pages,
13      total_count: @articles.total_count
14    }
15  }
16end

Rails makes building APIs clean and maintainable!