Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to implement API versioning with active_model_serializers

I know there are already some questions and also this is a open issue regarding AMS not handling namespaces too efficiently (which is used by this versioning approach) but I wanted to be sure I am in the right track within the current constraints.

Right now I am using Rails 5 and AMS 0.10.1, so I did the following:

# config/initializers/active_model_serializer.rb
ActiveModelSerializers.config.serializer_lookup_enabled = false

to disable default serializer lookup (which didn't work anyway); and

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  def get_serializer(resource, options = {})
    unless options[:each_serializer] || options[:serializer] then
      serializer = (self.class.name.gsub("Controller","").singularize + "Serializer").constantize
      resource.respond_to?(:to_ary) ? options[:each_serializer] = serializer : options[:serializer] = serializer
    end
    super(resource, options)
  end
end

to override how serializers are found by default; my controllers and serializer are like this:

# app/controllers/api/v2/api_controller.rb
module Api::V2
  class ApiController < ApplicationController
    ...

# app/controllers/api/v2/users_controller.rb
module Api::V2
  class UsersController < ApiController
    ...

and

# app/serializers/api/v2/user_serializer.rb
module Api::V2
  class UserSerializer < ActiveModel::Serializer
    ...    

Now, things like ActiveModel::Serializer.serializer_for(object) won't work, so I had to also monkey patch my request specs using example.metadata[:api_version] to set the API version before each test and raising and error if the example didn't set it.

So:

  1. Is there a better way documented?
  2. Is this any close to being correct?
  3. Will I be facing problem further in with this approach?
  4. How can it be improved?
like image 951
chrisandrew.cl Avatar asked Jun 23 '16 20:06

chrisandrew.cl


1 Answers

I think what you have here is okay. I am using the same approach and it works fine for my application. I picked the original idea here from Ryan Bates where he did explains very similar approach

http://railscasts.com/episodes/350-rest-api-versioning

This is what I use to specify different serializers for each resource:

module API
  module V3
    class AssetController < API::V3::ApiController
      def index
        render json: assets, status: :ok, each_serializer: API::V3::Serializers::AssetSerializer
      end
  end
end

In my implementation I am using serializers inside api/controllers/api/v3/serializers. So you are versioning serializers classes and controller classes

Not sure that you really need to have get_serializer since this is more explicit but not a big deal

If you have a lot of api endpoints try to organize them in resources. In my config/routes.rb I have about 700 resources so I split them into separate files config/api/v1/routes.rb...

namespace :api, defaults: {format: 'json'} do
  namespace :v1
    resources :assets
  end
end

Also It is handy to do inside inflections.rb initializer

ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym 'API'
end

For me I would say that the biggest important issue is to have good testing coverage. I prefer spec and check for correct status codes 200, 201,...etc and as well as the correct son output using json_schema

If you need to do auth then I would suggest that you use token based auth and JWT - JSON Web Token. In my implementation I am using two tokens. One token for reading and different token when doing POST and PATCH (not sure it is needed maybe). so inside API controller something like this

class ApiController < ActionController::Base
  skip_before_action :verify_authenticity_token, if: :json_request?
  before_action :authenticate

  protected
  def json_request?
    request.format.json?
  end
  if request.headers['X-Authorization']
    token = request.headers['X-Authorization']
    payload = JWT.decode(token, 'my_custom_key_to_check_if_key_has_been_tempered d_on_client_side')[0]
  end
end
like image 72
Mirza Memic Avatar answered Sep 30 '22 06:09

Mirza Memic