开发者

Ensuring APIs are separate

开发者 https://www.devze.com 2023-02-03 21:52 出处:网络
TL;DR: Crafting an API. Need different fields for different versions. Teach me, wise ones. I\'m currently trying to figure out the best way to craft a versioned API. That is to say, I wish to have a

TL;DR: Crafting an API. Need different fields for different versions. Teach me, wise ones.

I'm currently trying to figure out the best way to craft a versioned API. That is to say, I wish to have a URL of /api/v1/projects.json that would show a list of projects with a bunch of fields and api/v2/projects.json to show a list of projects with separate fields.

I've been thinking about this problem for about 15 minutes which probably means it's all wrong. At the moment I've got this in my app/models/project.rb file:

def self.api_fields
  { 
    :v1 => ["name"],
    :v2 => ["name", "tickets_count"]
  }
end

Then I can use this in my API controllers (api/v1/projects_controller.rb) like this:

def index
  respond_with(Project.all(:select => Project.api_fields[:v1]))
end

This is great and works as I'd like it to, but there's probably a b开发者_高级运维etter way about it. That's your task! Share with me your mountains of API-crafting wisdom.

Bonus points if you come up with a solution that will also allow me to use methods for instances of a model's object, such as a tickets_count method on a Project method.


I'm agree with polarblau that you should have multiple controllers for different version of the API. So, I aim for the bonus point of this question.

I think to archive the ability to call #tickets_count, you have to override #as_json and #to_xml methods of the model. I think you'll have to do it like this:

api/v1/projects_controller.rb

def index
  respond_with Project.all, :api_version => :v1
end

project.rb

class Project < ActiveRecord::Base
  API_FIELDS = {
    :v1 => { :only => [:name] },
    :v2 => { :only => [:name], :methods => [:tickets_count] }
  }

  def as_json(options = {})
    options.merge! API_FIELDS[options[:api_version]]
    super
  end

  def to_xml(options = {}, &block)
    options.merge! API_FIELDS[options[:api_version]]
    super
  end
end

However, if you don't mind the mess in the controller, I think specifying :only and :methods in respond_with call in the controller might be a good idea too, as you don't have to override those #as_json and #to_xml methods.


Just as a comment:

Have you had a look a these yet?

http://devoh.com/posts/2010/04/simple-api-versioning-in-rails

Best practices for API versioning?

devoh.com suggest to split the versions already at a routing level, which seems like a good idea:

map.namespace(:v1) do |v1|
  v1.resources :categories
  v1.resources :products
end

map.namespace(:v2) do |v2|
  v2.resources :categories, :has_many => :products
end

Then you could use different controllers to return the different fields.


The problem is, as you know, that whatever you expose allows the end-client to create a direct dependency. Having said that, if you directly expose your models to the world, e.g. http://domain.com/products.json, whenever you change your Products model you have a limited number of options:

  1. The end-client must live with it and behave much like a "schemaless database". You say that it's going to change, and voila it's done (reads clients will have to deal with it)!
  2. You add a more enterprise-like versioning to your API. That's meas that at a more advanced level what you expose to the end-client are not your models. Instead you expose public objects, which in turn can be versioned. This is called a Data Transfer Object (http://en.wikipedia.org/wiki/Data_transfer_object)

If we wished to pursue the 2nd approach we could do the following:

class Project < ActiveRecord::Base
end

class PublicProject
  def to_json(version = API_VERSION)
    self.send("load_#{version}_project").to_json
  end

  private
    def load_v1_project
      project = load_v2_project
      # logic that transforms a current project in a project that v1 users can understand
    end

    def load_v2_project
      Project.find...
    end
end

Hope it helps.


Mount a Sinatra app in routes at /api/v1 to handle your API calls. Makes it easier to add a new API and still be backwards compatible until you deprecate it.

0

精彩评论

暂无评论...
验证码 换一张
取 消