Hello everybody! A little context, I’m very intrigued by Lotus and want to build a JSON backend for an Ember.js application. While the frontend application has client side validations to ensure correct user input the backend has to make sure that only valid data enters the application.
I’m looking for a way to handle validation errors and return them to the client if there is a problem.
In a classic web application where Lotus returns HTML to the client it is pretty clear. The view / form displays the errors to the user and also accesses the actions other exposures to pre-fill the form. When Lotus acts as an JSON backend this is a little different. A successful request gets an JSON object with the data and an 200 or 201 status code. A request with invalid parameters gets an 4xx status code with an JSON object containing the errors.
Example for an successful response:
{
"id": "1",
"email": "david@strauss.io"
}
Example for an unsuccessful response:
{
"errors": {
"email": ["must be set"]
}
}
I’m struggling to handle this situation in a nice way. You can view my current solution at the bottom of the post. My ideal solution is to halt the request or throw an exception so I can handle this scenario in one place. But compared to my solution it should not rely on the controller to build the JSON response. This should be an concern of the view, however I could not find a way to define a view for a specific exception / status code.
How would you solve this problem?
module Datsu::Controllers::Identities
class Create
include Datsu::Action
expose :identity
params do
param :email, presence: true
end
def call(params)
halt 422, Datsu::Views::Errors::UnprocessableEntity.render(exposures) unless params.valid?
@identity = {
id: 1,
email: 'david@strauss.io'
}
end
end
end
module Datsu::Views::Errors
class UnprocessableEntity
include Datsu::View
format :json
def render
mapped_errors = errors.to_h.inject({}) { |hash, (key, value)| hash.merge({ key.to_sym => value.map(&:validation) }) }
JSON.generate({ errors: mapped_errors.to_h })
end
end
end
@stravid Hello. I’d solve the issue in this way:
module Preconditions
def self.included(action)
action.class_eval do
before :validate!
end
end
private
def validate!
error(412) unless params.valid?
end
def error(code)
# This is an example, replace it according to your needs
message = JSON.generate(params.errors)
halt code, message
end
end
Datsu::Application.configure do
controller.prepare do
include Preconditions
end
end
It will add this validation logic for ALL the actions in Datsu
application.
When you don’t use use param validations, params.valid?
always returns true
. This logic is basically a no-op.
When you use validations, it may throw 412
via #error
.
Lotus::Action#halt
can be used with a valid HTTP code only. For instance halt 404
, it will return a message "Not Found"
by default. When used with two args, it allows to customize the body of the response. Example: halt 412, "Oh no!"
.
I’d not worry about having this logic in a view, the code presented here is faster. I see it as a result of a computation, not as presentational logic. If you’re still not convinced, I’d use a custom error serializer.
Thank you very much for the response, this works nicely! And your argument why it’s not a concern of the view is compelling.
One more question, where in the directory structure would you store the module?
A good candidate can be
apps/datsu/controllers/preconditions.rb
Remember to explicit do require_relative
from apps/datsu/application.rb
.
@jodosha Another question came up regarding uniqueness validations. I’m handling them on the database level with an unique index. When this rule is violated an Sequel::UniqueConstraintViolation
error is raised.
How would you handle this in an action? (Taking into consideration that the client should get an response with the errors.)
As far as I can tell handle_exception
doesn’t work in this case since it only takes an HTTP status code. Then I tried to add the error to the errors but could not find an appropriate public API within lotus/validation
. My current solution looks like the following, I’m hoping it can be DRY’ed up a little bit.
module Datsu::Controllers::Identities
class Create
include Datsu::Action
expose :identity
params do
param :identity do
param :email, presence: true
param :password, presence: true
end
end
def initialize(repository = IdentityRepository)
@repository = repository
end
def call(params)
@identity = Identity.new params[:identity]
@repository.create @identity
rescue Sequel::UniqueConstraintViolation
halt_with_error 'identity.email', :uniqueness
end
end
end
module ParameterValidation
private
def validate!
halt_with_errors(error_map) unless params.valid?
end
def error_map
errors.to_h.inject({}) { |hash, (key, value)| hash.merge({ key.to_sym => value.map(&:validation) }) }.to_h
end
def halt_with_errors(errors)
halt 422, JSON.generate({ errors: errors })
end
def halt_with_error(attribute, error)
halt_with_errors({ attribute => [ error ] })
end
end
I suggest to use Lotus::Interactor
. Now your action (HTTP abstraction) is concerned with database details.
class CreateIdentity
include Lotus::Interactor
def initialize(params)
@identity = Identity.new(params[:identity])
end
def call
persist_identity
end
private
def persist_identity
@identity = IdentityRepository.create(@identity)
rescue Sequel::UniqueConstraintViolation
error! "Your error message here"
end
end
module Datsu::Controllers::Identities
class Create
include Datsu::Action
def initialize(interactor = CreateIdentity)
@interactor = interactor
end
def call(params)
result = interactor.new(params).call
if result.success?
# ...
else
result.error
end
end
end
end
Thanks for the input, broadly spoken this means I should treat parameter validation and violations of the unique constraint as two separate things, correct?
Is there a specific reason the IdentityRepository
is used without dependency injection in the interactor?
I don’t see the reason. My personal rule for testing interactors is to mock nothing. I want to hit the database and use real collaborators.