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 --apiThis 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
48endRoutes
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
9endAPI 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 # ...
33endJSON 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
9endUsing 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
16endruby
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
12endUsing 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
20endAPI 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
27endJWT 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
48endCORS 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
14endRate 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
13endPagination
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 }
16endRails makes building APIs clean and maintainable!
