When the Web goes Javascript
and you want nothing to do with it
rails new --api rails-api-demo
API controllers inherit from
ActionController::API
instead of
ActionController::Base
class API::ApplicationController < ActionController::API
# ...
end
Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
get "/hello-world", to: "hello#world"
end
end
# /app/controllers/api/hello_controller.rb
class Api::HelloController < Api::ApplicationController
def world
render json: { hello: "world" }
end
end
You don't have cookies, so
each request is completely
stateless.
Authentication proof has
be sent in the request.
Json Web Token is the standard. It encrypts whatever information the system needs for authentication.
gem install jwt
# config/routes.rb
Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
get "/hello-world", to: "hello#world"
# NEW:
post "/authenticate", to: "access_tokens#create"
end
end
# app/controllers/api/access_tokens_controller.rb
class Api::AccessTokensController < Api::ApplicationController
def create
params.require(%i[email password])
user = User.find_by(email: params["email"])
unless user&.authenticate(params["password"])
raise StandardError, "Either email or password is incorrect"
end
render json: { jwt: AccessToken.encode(user) }, status: :created
end
end
# app/models/access_token.rb
class AccessToken
def self.encode_user(user)
payload = {
user_id: user.id,
exp: 10.minutes.from_now.to_i,
}
JWT.encode(payload, jwt_password)
end
def self.jwt_password
Rails.application.secret_key_base
end
end
# /app/controllers/api/hello_controller.rb
class Api::HelloController < Api::ApplicationController
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_user!
# def world...
private
def authenticate_user!
authenticate_or_request_with_http_token do |token|
@current_user = AccessToken.decode_user!(token)
end
end
end
# app/models/access_token.rb
class AccessToken
# def self.encode_user...
def self.decode_user!(token)
payload = JWT.decode(token, jwt_password).first
User.find(payload["user_id"])
end
# def self.jwt_password...
end
HTTP interfaces use status codes to classify the result of a request.
APIs must leverage these
codes, and complement them if possible, to provide context in case of errors.
The Rails API approach is focused on the happy path.
When errors occur, exceptions will interrupt the normal flow.
Handling these exceptions allows us to customize the responses.
# app/controllers/api/application_controller.rb
class API::ApplicationController < ActionController::API
rescue_from ActionController::ParameterMissing do |error|
render_error(error&.message, :bad_request)
end
private
def render_error(reason, status)
render json: { reason: reason }, status: status
end
end
# app/controllers/api/application_controller.rb
class API::ApplicationController < ActionController::API
class AuthenticationError < StandardError end
# rescue_from ActionController::ParameterMissing...
rescue_from AuthenticationError do |error|
render_error(error&.message, :bad_request)
end
# private def render_error...
end
# app/controllers/api/access_tokens_controller.rb
class Api::AccessTokensController < Api::ApplicationController
def create
# params.require...
# user = User.find_by...
# unless user&.authenticate...
raise AuthenticationError, "Either email or password is incorrect"
end
# render json: ...
end
end
# app/controllers/api/application_controller.rb
class API::ApplicationController < ActionController::API
# class AuthenticationError ...
# rescue_from ActionController::ParameterMissing...
# rescue_from AuthenticationError...
rescue_from AccessToken::ValidationError do |error|
render_error(error&.message, :unauthorized)
end
# private def render_error...
end
# app/models/access_token.rb
class AccessToken
class ValidationError < StandardError; end
# def self.encode_user...
def self.decode_user!(token)
# payload = JWT.decode(token, jwt_password).first
# User.find(payload["user_id"])
rescue ActiveRecord::RecordNotFound, JWT::ExpiredSignature, JWT::DecodeError
raise ValidationError, "Provided access token is not valid"
end
# def self.jwt_password...
end