A simple way to organise API endpoints
EndpointFlux offers you a new file and logic organisation in Ruby applications.
app
├── controllers
│ ├── users_controller.rb
├── endpoint_flux
│ ├── decorators
│ │ ├── users
│ │ │ ├── project.rb
│ │ ├── user.rb
│ │ ├── ...
│ ├── endpoints
│ │ ├── users
│ │ ├── create.rb
│ │ ├── update.rb
│ │ ├── ...
│ ├── middlewares
│ │ ├── authenticator
│ │ │ ├── default.rb
│ │ ├── authorizator
│ │ │ ├── default.rb
│ │ ├── decorator
│ │ │ ├── paginate.rb
│ │ │ ├── representable.rb
│ │ │ ├── ...
│ │ ├── policy
│ │ │ ├── comment.rb
│ │ │ ├── ...
│ │ ├── validator
│ │ ├── inline.rb
│ ├── services
│ │ ├── auth.rb
│ │ ├── ...
│ ├── validations
│ │ ├── predicates
│ │ │ ├── base.rb
│ │ │ ├── date.rb
│ │ │ ├── ...
│ │ ├── base.rb
│ │ ├── error.rb
│ │ ├── user.rb
Add this line to your application's Gemfile:
gem 'endpoint-flux'
And then execute:
$ bundle
Or install it yourself as:
$ gem install endpoint-flux
You can initialize the EndpointFlux before using and can specify a bunch of params as you need. Locate it in
config/initializers/endpoint_flux.rb
.
With the EndpointFlux.config.middlewares_namespaces
directive you can specify the location of middleware.
EndpointFlux.config.middlewares_namespaces << 'middlewares'
To specify the default middleware that will be used for each response, if nothing specified in Endpoint class, use
EndpointFlux.config.default_middlewares
directive. For example:
EndpointFlux.config.default_middlewares :validator, :inline
Where :validator
is a middleware and :inline
is a class that defined in
app/endpoint_flux/middlewares/validator/inline.rb
. More details here.
With EndpointFlux.config.rescue_from
you can specify how to handle the custom exceptions that would be raised in
Application. For example:
not_found_errors = [ActiveRecord::RecordNotFound]
EndpointFlux.config.rescue_from(not_found_errors) do |_, attrs, _|
attrs[1].body = EndpointFlux::Exceptions::NotFound.new.to_hash
attrs
end
Also you can specify interceptor that would be called before processing each response
EndpointFlux.config.interceptor do |attrs|
Rails.root.join('maintenance.txt').exist? &&
raise(EndpointFlux::Exceptions::ServiceUnavailable)
attrs
end
And if you need you can define your own methods like this:
EndpointFlux::Endpoint.class_eval do
define_method(:raise_validation_error) do |errors|
raise EndpointFlux::Exceptions::Validation, errors
end
end
Config example:
# config/initializers/endpoint_flux.rb
require 'endpoint_flux'
EndpointFlux.config.middlewares_namespaces << 'middlewares'
EndpointFlux.config.default_middlewares :authenticator, :default
EndpointFlux.config.default_middlewares :authorizator, :default
EndpointFlux.config.default_middlewares :validator, :inline
EndpointFlux.config.default_middlewares :policy, :skip
EndpointFlux.config.default_middlewares :decorator, :skip
not_found_errors = [ActiveRecord::RecordNotFound]
EndpointFlux.config.rescue_from(not_found_errors) do |_, attrs, _|
attrs[1].body = EndpointFlux::Exceptions::NotFound.new.to_hash
attrs
end
EndpointFlux.config.interceptor do |attrs|
Rails.root.join('maintenance.txt').exist? &&
raise(EndpointFlux::Exceptions::ServiceUnavailable)
attrs
end
EndpointFlux::Endpoint.class_eval do
define_method(:raise_validation_error) do |errors|
raise EndpointFlux::Exceptions::Validation, errors
end
end
EndpointFlux::Request.class_eval do
attr_accessor :current_user_params
define_method(:current_user) do
@current_user ||= begin
if current_user_params && current_user_params['id']
User.find_by(id: current_user_params['id'])
end
end
end
end
EndpointFlux has Rails helper -
present
,
which integrates with Rails controllers. So, you can use the default Rails routing system to define routes.
Rails.application.routes.draw do
resources :users
end
class UsersController < ApplicationController
def index
present 'users/index' # it dispatches to Endpoints::Users::Index endpoint class.
end
end
Or if you're using it in not Rails application, you can implement the middleware for providing such data to Endpoints namespace, for example:
class BaseHandler
def process(msg, options, namespace)
params = JSON.parse(msg)
action = options[:headers]['action']
endpoint = endpoint_for("#{namespace}/#{action}")
_, response = endpoint.perform(request_object(params))
response.body
end
private
def endpoint_for(namespace)
if ::EndpointFlux.config.endpoints_namespace
::EndpointFlux.config.endpoints_namespace + '/' + namespace
else
namespace
end.camelize.constantize
end
def request_object(params)
::EndpointFlux::Request.new(headers: {}, params: params.to_h.deep_symbolize_keys!)
end
end
Controllers are simple endpoints for HTTP. They don't have any business logic and just dispatch to an endpoint class.
class UsersController < ApplicationController
def index
present 'users/index' # it dispatches to Endpoints::Users::Index endpoint class.
end
end
Finally present
method renders response.body
in JSON format (render json: response.body
).
Endpoints encapsulate business logic and it's a central part of applications architecture. It can be used in any Ruby
application like Rails, Sinatra and etc.
It's a simple coordinator between all layers needed to get the job done. Endpoint needs for defining and implementing
steps for the processing data for response. It uses middlewares for data processing that receives the arguments from
the caller and returns the array [request, response]
with response
that contains body
and headers
for API.
# app/endpoint_flux/endpoints/users/comments/index.rb
module Endpoints
module Users
module Comments
module Index
include EndpointFlux::Endpoint
policy :user
policy :comments
validator :inline do
required(:user_id).value(:number?)
end
process do |request, response|
response.body[:comments] = request.scope
[request, response]
end
decorator :add_status, 200
decorator :representable, decorator: :comment, collection?: true, wrapped_in: :comments
end
end
end
end
EndpointFlux has 6 types of predefined middlewares. They will be called in the strong defined order - authenticator, authorizator, validator, policy, process, decorator
. Where process
should be defined inside the endpoint class for
the request processing.
Also you can add your own middleware class to this flow or change the order. It's possible in two ways:
- Inside the custom endpoint class, for example:
# app/endpoint_flux/endpoints/users/index.rb
module Endpoints
module Users
module Index
include EndpointFlux::Endpoint
# define new flow with `new_middleware` only for this endpoint
flow %i[authenticator authorizator validator policy process new_middleware decorator]
authorizator :skip
#...
process do |request, response|
# ... some actions
[request, response]
end
# define the middleware
new_middleware :default
decorator :add_status, 200
end
end
end
- Globally in EndpointFlux config section that will affect all endpoints, for example:
# config/initializers/endpoint_flux.rb
# ...
# define default value for new middleware
EndpointFlux.config.default_middlewares :new_middleware, :default
# Global change the middlewares order flow by adding a new one `new_middleware`
EndpointFlux.config.flow(%i[authenticator authorizator validator policy process new_middleware decorator])
Middleware class definition should contains self.perform(*args)
method and returns the [request, response]
as a result
# app/endpoint_flux/middlewares/new_middleware/default.rb
module Middlewares
module NewMiddleware
module Default
def self.perform(request, response, _)
# ... some actions
[request, response]
end
end
end
end
We have implemented the default middlewares that could be used to skip it without any changes to data. It's located here
Here you can implement your authenticate system. For example you can user the JWT gem
Locate it in app/endpoint_flux/middlewares/authenticator
folder.
Also you can skip this middleware in Endpoint class by authenticator :skip
directive
Here you can implement your authorization system and check the user permissions according to the user role.
Locate it in app/endpoint_flux/middlewares/authorizator
folder.
Also you can skip this middleware in the Endpoint class by authorizator :skip
directive
Here you implement different policy scopes and use them inside the Endpoint class. And also you can chain it to each other by
calling in special order. Locate it in app/endpoint_flux/middlewares/policy
folder.
For example:
- User policy
# app/endpoint_flux/middlewares/policy/user.rb
module Middlewares
module Policy
module User
def self.perform(request, response, _)
request.scope = ::User.find(request.params[:user_id])
[request, response]
end
end
end
end
- Comments policy
# app/endpoint_flux/middlewares/policy/comments.rb
module Middlewares
module Policy
module Comments
def self.perform(request, response, _)
raise 'scope must be set' unless request.scope
raise 'scope must be User' unless request.scope.class.name == 'User'
request.scope = ::Comment.where(user_id: request.scope.id)
[request, response]
end
end
end
end
And usage inside the Endpoint class:
# app/endpoint_flux/endpoints/users/comments/index.rb
module Endpoints
module Users
module Comments
module Index
include EndpointFlux::Endpoint
policy :user # get user scope
policy :comments # get users comments scope
validator :inline do
required(:user_id).value(:number?)
end
process do |request, response|
response.body[:comments] = request.scope
[request, response]
end
decorator :add_status, 200
decorator :representable, decorator: :comment, collection?: true, wrapped_in: :comments
end
end
end
end
Here you can implement validation system for request params using the Dry validation gem
or another libraries. Locate it in app/endpoint_flux/middlewares/validator
folder.
Also you can skip this middleware in the Endpoint class by validator :empty
directive
# app/endpoint_flux/middlewares/validator/inline.rb
module Middlewares
module Validator
module Inline
def self.perform(request, response, _options, &block)
validation = ::Services::Validation(&block).call(request.params)
unless validation.success?
raise ::EndpointFlux::Exceptions::Validation, validation.messages
end
request.params = validation.result
[request, response]
end
end
end
end
Just declare the schema block inside the endpoint to provide it to middleware
# app/endpoint_flux/endpoints/users/create.rb
module Endpoints
module Users
module Create
include EndpointFlux::Endpoint
authenticator :skip
authorizator :skip
validator :inline do
required(:user).schema do
required(:email).value(:str?, :email?)
required(:password).value(:str?, :password?)
end
end
process do |request, response|
# some actions ... like calling checking for user uniqueness, Mailer Sidekiq workers, token generation and etc.
response.body[:user] = ::User.create(request.params[:user])
# ...
[request, response]
end
decorator :add_status, 200
decorator :representable, decorator: :user
end
end
end
Here you can implement a decorator system for representing the response.
Locate it in app/endpoint_flux/middlewares/decorator
folder. You can call it inside the endpoint class by using
directive like this decorator :representable, decorator: :user
, where :representable
it's your decorators class name
and decorator: :user
it's a custom params as you wish (in this situation specialising to use User decorator for
representing data).
For example
# app/endpoint_flux/middlewares/decorator/representable.rb
module Middlewares
module Decorator
module Representable
def self.perform(request, response, options)
resource_name = options[:decorator]
resource = response.body[resource_name]
response.body[resource_name] = ::Services::Decorator.call(resource, options) if resource
[request, response]
end
end
end
end
You can add a custom status to the response body by using directive decorator :add_status, {status_number}
,
for example decorator :add_status, 200
.
Also you can skip this middleware in the Endpoint class by decorator :skip
directive
Endpoint can use representers from app/endpoint_flux/decorators
to serialize and parse JSON and XML documents for APIs.
For example you can use
Representable gem, it maps representation documents from and to Ruby objects
and includes JSON, XML and YAML support, plain properties and compositions.
You can define the decorator schema class in app/endpoint_flux/decorators
folder and specify it inside of the
endpoint class by providing as params for decorator
directive,
for example decorator :representable, decorator: :user
# app/endpoint_flux/decorators/user.rb
module Decorators
class User < Representable::Decorator
include Representable::JSON
property :id
property :name
property :email
property :role, exec_context: :decorator
property :updated_at
property :created_at
def role
represented.role.name
end
end
end
In app/endpoint_flux/validations
you can locate a custom validation classes and use them with Validator middleware.
You can move some business logic from endpoints to service object and locate it here app/endpoint_flux/services
.
You can use EndpointFlux predefined Exceptions for you business logic, for example
raise ::EndpointFlux::Exceptions::Validation
.
They defined in
lib/endpoint_flux/exceptions
The list of exceptions:
Forbidden
NotFound
ServiceUnavailable
Unauthorized
Validation
If needs you can use the response helpers to check the response body status such as success?
, invalid?
or
you can define your own helpers in that way. You can use it with an instance of the EndpointFlux::Response
class.
They defined in
lib/endpoint_flux/response.rb
The list of helpers:
success?
invalid?
forbidden?
unauthorized?
not_found?
The gem is available as open source under the terms of the MIT License.