Rails 5+ goodies for APIs
When the Web goes Javascript
and you want nothing to do with it
rails-api
Rails 5+
rails new --api rails-api-demo
Getting started
API controllers inherit from
ActionController::API
instead of
ActionController::Base
class API::ApplicationController < ActionController::API
# ...
end
Batteries included
- Rendering (all formats);
- Redirection;
- URL generation;
- Conditional GETs;
- Strong parameters;
- Data streaming;
- Default headers;
- Callbacks;
- Exception handling;
- Instrumentation;
- more
Bloat free
- No implicit template rendering;
- No translations;
- No asset stuff;
- No view pipeline;
- Caching;
- No cookies;
- No authentication;
-
yet everything is opt-in
Hello world!
Routing
- No special care needed;
- Recommended:
- Namespaced routes;
- Default format!
Rails.application.routes.draw do
namespace :api, defaults: { format: :json } do
get "/hello-world", to: "hello#world"
end
end
Controller
- Implicitly responds with
204 No Content - Can render templates;
- Recommended:
- Specify valid format.
(if not using templates)
- Specify valid format.
# /app/controllers/api/hello_controller.rb
class Api::HelloController < Api::ApplicationController
def world
render json: { hello: "world" }
end
end
✔
Authentication
Token auth
You don't have cookies, so
each request is completely
stateless.
Authentication proof has
be sent in the request.
JWT
Json Web Token is the standard. It encrypts whatever information the system needs for authentication.
gem install jwt
Get an access token
# 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
Get an access token
# 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
Get an access token
# 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
✔
Authenticate the user
# /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
Authenticate the user
# 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
✔
Error Handling
Status codes
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.
Exceptions
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.
Bad request
# 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
Conflict
# 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
Conflict
# 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
Unauthorized
# 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
Unauthorized
# 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
✔
BAAM!
Rails 5+ goodies for APIs
By pfac
Rails 5+ goodies for APIs
- 124