Ensuring APIs are separate
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:
- 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)!
- 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.
精彩评论