What is this? I'm a little bugged out by all current approaches that are available to build JSON API APIs. I'm writing all this down to help me figure out what I want and how to accomplish it.
There are basically two sides, reading data and writing data. There are also a few parts of the stack that can be generalized. On the other side there are also parts where I think generalization is harmful.
Things like transforming parameters from dasherized to snake case, and serializing an object graph to JSON can be kept pretty generic. The thing I'm not so sure about on the read side is handling query parameters like included
, fields
, sort
, filter
, page
and so on. For example using JSONAPI::Resources gives you the query parameter handling out of the box. On the other side I think it's impossible to deal with associations if you don't use the Active Record pattern and/or have 1:1 mapping between API resources and models.
On the write side I'm pretty much against generic solutions since they break immediately once you leave CRUD. I do think it would be useful to have a generic stack, like middlewares, with a well defined place and interface for your business logic.
In general I don't want to validate parameters by hand or have to react to exceptions and serialize them.
- If desired, authenticate the request.
- Transform parameters to snake case.
- Pass parameters to application and get object graph returned.
- Serialize object graph.
Ideas: Models/Repositorys have to implement a query method that get query parameters passed in and know how to deal with associations, sorting, pagination and all the other generic stuff.
- If desired, authenticate the request.
- Transform parameters to snake case.
- Validate incoming parameters. (Short circuit if there are problems.)
- Pass parameters to application and get object graph returned.
- Serialize object graph.
There are a lot of placeholders and dummy implementations that will be replaced by real pieces of code like dry-validations
for parameter handling.
module TransformParameters
def call(params)
hash = params.to_hash
super Hash[hash.map { |k, v| [k.gsub('-', '_'), v] }]
end
end
module ValidateParameters
def call(params)
halt_with_errors({ code: "missing parameter" }) unless params.key?("query_name")
super params
end
end
module AuthenticateRequest
def call(params)
halt_with_errors({ code: "unauthorized" }) unless authenticated?
super params
end
def authenticated?
true
end
end
module ErrorHandling
def halt_with_errors(errors)
halt 422, errors.to_json
end
end
module JSONAPIResponse
def jsonapi_response(status_code, object_graph)
self.status = status_code
self.body = object_graph.to_json
end
end
module WriteAction
def self.included(base)
base.prepend ValidateParameters
base.prepend TransformParameters
base.prepend AuthenticateRequest
base.include ErrorHandling
base.include JSONAPIResponse
base.include Api::Action
end
end
module Api::Controllers::Commands
class Create
include WriteAction
def call(params)
# Pass params to your applications entry point and let it do what it does
jsonapi_response(200, { data: { type: "person" } })
end
end
end