Correct way to implement API versioning with active_model_serializers
Asked Answered
D

2

6

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?
Dumont answered 23/6, 2016 at 20:1 Comment(0)
D
0

Since I haven't found a better way, neither documented nor anywhere, it also seems to be correct and I haven't faced problems after a while using it, this appear to be a good approach for API versioning.

Anyway, I advise caution using this approach to not change behaviour of older supported versions for your API. Test carefully and notify your clients for deprecations and support removal for old versions.

Dumont answered 1/12, 2016 at 14:15 Comment(0)
R
6

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
Rochellerochemont answered 23/6, 2016 at 20:33 Comment(1)
Thank you for you input! I've done that to DRY up things a little. get_serializer is always called by AMS and I only need to use render json: @object, serializer: Namespaced::Unconventional::ObjectSerializer if it doesn't follow the defined convention.Dumont
D
0

Since I haven't found a better way, neither documented nor anywhere, it also seems to be correct and I haven't faced problems after a while using it, this appear to be a good approach for API versioning.

Anyway, I advise caution using this approach to not change behaviour of older supported versions for your API. Test carefully and notify your clients for deprecations and support removal for old versions.

Dumont answered 1/12, 2016 at 14:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.