#350 REST API Versioning
APIs should be consistent, but it is difficult to do this when returning a JSON response along side the HTML interface. Here I show how to add a versioned, RESTful API. The version can be determined from either the URL or HTTP headers.
- Download:
- source code
- mp4
- m4v
- webm
- ogv
Nice one Ryan,
Would be good to expand this tutorial on how to secure your API with tokens or usernames and passwords? Also the possibility of using CanCan to authorize the api methods.
I would have it a guess the best place to start would be to create a api base class where most of that work could be done?
securing with OAuth2 please :)
thanks for the episode!
Yes expanding the API with OAuth 2 would be awesome!
Can I suggest Doorkeeper for OAuth2?
https://github.com/applicake/doorkeeper
Why not just sign all params on the client and server side using SHA1?
Client:
api_key = SHA1(params, secret_salt, date)
Server:
SHA1(params, secret_salt, date) == api_key
Interesting. However, the date part of the hash will probably leave you open for re-play attacks.
Yes. I second that; adding security features.
And thanks for the cast Ryan :)
I third/fourth that. OAuth2 security for your API please.
I'm fifth-ing that! tokens and OAuth 2 please.
at my day job, i just created an OAuth 2.0 provider and consumer demo with the help of this tutorial http://unhandledexpression.com/2011/06/02/rails-and-oauth-plugin-part-1-the-provider/ & http://unhandledexpression.com/2011/06/28/rails-and-oauth-plugin-part-2-the-consumer/ and that tutorial is for sqlite or mysql.
but i needed to do it for MongoDB using Mongoid so you can see all of the little changes here that i had to make to get it to work https://github.com/ndgiang84/oauth-sample
I am hoping to cover authenticating/authorizing an API soon. Thanks for the suggestion.
great! how soon do you think this could happen? we are currently thinking in doing it and I wonder if we should wait for your cast...
thanks a lot for all the great videos!
-cristobal
Cool! Thanks Ryan!
Definitely interested in this
Great one. Super useful.
Just one comment: lib/api_constraints.rb has a typo at line 3 @verison instead of @version.
Fixed, thanks for bringing this to my attention.
One thing I have started doing recently for a few APIs is to support versioning simply using folders with RABL templating. Depending on your situation, it can greatly reduce code duplication. Simply setup a single api controller and then render the corresponding template based on a version parameter i.e
render "#{params[:version]}/show"
and then create templates for each version (users/v1/show
,users/v2/show
). Since the controller between versions is typically largely unchanged in most cases, this may be an alternative approach. If anyone is curious for more details, perhaps I can add a guide to the RABL wiki.Nice!
We are working with a similar approach. I agree that ideally we should focus on payload changes. Check out the versioncake gem: https://github.com/bwillis/versioncake
Each time you create a new MIME type name (
application/vnd.example.v1
,application/vnd.example.v2
, ...), a kitten is hurt! don't do that! that's bad!Use MIME parameters instead:
application/vnd.example; version=1
,application/vnd.example; version=2
, ... (version=
, orlevel=
, orv=
or whatever you want, this is part of your design).The semantic of a MIME type is that every document "flagged" with that type share the same "lineage", and that two different names are not related, even if they share a same prefix.
By incorporating a version identifier into the MIME type name, you are making the names distinct and are in the same case as if your V1 API was using
text/html
documents whereas your V2 now usesimage/png
pictures instead.This is interesting, I'll have to do some research. Thanks for sharing.
Problem is this format wasn't properly supported in Rails until very recently, and I don't think the fix has actually been released yet.
https://github.com/rails/rails/issues/4127#issuecomment-4167613
Great episode!
But there is a bug in lib/api_constrains.rb :
def initialize(options)
@verison = options[:version]
@default = options[:default]
end
Should it not be the following?:
@version = options[:version]
because in the matches method below it, you use the @version variable...
Oops, just saw David spotted it also ;)
Fixed, thanks for bringing this to my attention.
Ryan, you just read my mind! I've been looking for a good resource on best practice for this so having you create a post like this is a real treat since there's not much info for best practices.
Does anyone know a good technique for auto-generating API documentation?
Andy, take a look on this.
Ryan, thanks so much for recording this episode. Last week I worked in something similar. Just one thing, I would pass the version as a mime type parameter. Something like:
curl -H "Accept: vnd.myapp+json; version=2"
Here's my implementation.
Do you have any recommendations for handling errors using the correct content type within an API namespace? For example if you 404 by POSTing to /api/products/1 instead of using a PUT, you get the Rails HTML 404 page in the response because the route does not match. Same goes for 500 errors if they ever happen.
Would be nice to get a JSON response for all requests in the namespace.
+1 That as well
+1
I'm just stoked about learning about defaults on namespaces in the routes...
that's a great tip.
Have you tried using the rescue_from method for this?
Thanks Ryan for the examples. Some clients will use multiple Accept header values when they expect to live in mixed environments, so it would be good to put the default version higher in the routes (in the version I read just now, it looks like a client sending v1 and v2 in the Accept header will get v1).
Not sure how you would efficiently use the q parameter (i.e. Accept: application/vnd.myapp+json;v=2;q=1.0,application/vnd.myapp+json;v=1;q=0.9), as ideally the server would select the most-preferred client version that it also supports, but that seems like a lot of overhead just to get strict correctness.
The problem with the default is it will accept any mime type, as it will only check the boolean and not the string:
I mean, if we're expecting "application/vnd.github.v1", it will also accept "application/vnd.twitter.v1"
Not being a Rails Expert, why did you wrap the different API Versions in a Module rather than just using a controller?
How would you handle overriding the as_json method for a Polymorphic model?
Once I try to override the query changes to :
Trying to make it works with mongoid. It doesn't seems to work. Product is always empty. What am I doing wrong?
Anyone using mongoid ?
I think, I have the same problem as Eran Kampf
How to convert / remove "Api::V1::ProductsController::Products" ?
Is there a way this can be implemented where you only define new functionality in new versions? Say for example:
I have a Users controller and a Products controller. Products has stayed the same between V1 and V2, but Users has changed. It would be nice if we could set it up so that if I call for V2 of Products, my application knows it doesn't exist and simply uses the most recent controller version instead (in this case, V1).
Make a generic api controller and inherit from that. Then override whatever you need to in the subclasses.
Have anyone tried using grape? It's built directly on top of Rack, which would seem to make it quite efficient. I think it also has an interface for oauth authentication.
I dont know how about you guys but i cant seem to get my tests to work.
How do you guys write these tests if your controller is nested inside the modules?
Obviously because the correct route should be:
{:action=>"index", :controller=>"api/v1/products"}
By the way API::V1::ProductsController will not just work.
Ok I figured it out after a weekend relaxing :p
You can put in your rspec:
This will perfectly work. In my other attempts before the weekend i placed
do
behind V1 and Api, that will just not work. :)I like using the idea of using the
Accepts
header for versioning, although it does make it harder to test API versions from a browser's URL bar.But there's a bug in this code that will be exposed once you reach v10.
matches?(req)
will return true when the router asks if the request is for v1, because'application/vnd.example.v10'.includes?('application/vnd.example.v1')
is true.One way to fix this is to reverse the order of your routes file: put newer versions at the top, not at the bottom.
For some reason I can't get my app to work based on the sample code for this screencast. After setting up the routes, api_constraints, and the api/v1/ controller directory structure, API requests fail with the exception:
My controller at
app/controllers/api/v1/groups_controller.rb
looks like this:My Routes look like this:
Anyone else have this problem? Have I missed something?
Solved
The problem was on my end. One of the gems my app depends on was overriding the String#constantize method, and it was breaking the resolution of the routing namespace. Removing that gem fixes my problem.
Sorry for the bother.
I can access the '/api/resource.json' but there is an error while trying access '/api/v2/resource.json'
No route matches [GET] "/api/v1/companies/00000000000001/reports.json"
No route matches [GET] "/api/v2/companies/00000000000001/reports.json"
I implemented this for a new API I'm building and my tests caught what I think might be a bug in the case where your default is an earlier version but you want to make later versions available. For example, you have v1, v2, and v3. v2 is still the default and you're testing v3. Because the matcher goes in order, when it hits the default (v2) it will match to that, even if you specified 'v3' in the Accepts header.
Here is how I solved it. I'm a bit of a n00b, so I'm sure you can make it prettier/more efficient.
Created an ACTIVE_API_VERSIONS constant so I know whether the version specified is active, in which case I can ignore the default.
Modified the class like so
With the default routing:
resources :products
you'll get routes for #new and #edit, which aren't used in a pure JSON interface. Is there a reason for NOT doing
?
Is it really a good practice to have a default version? There seems to be a big risk in not forcing clients to make a conscious decision on which API version to support since changing the default version will invalidate all clients accessing the API via the default route.
Why put everything in a separate controller for API? Image a separate API registration controller and a standard web controller; changes are made to the web controller forgetting to make the same changes to the API controller.
This is the problem that the respond_to blocks are suppose to help solve.
Take a look at VersionCake ( https://github.com/bwillis/versioncake ); easily set the API version in the request header. The appropriate view is then rendered for the request (e.g. show.v1.json.jbuilder). If the requested version of the view doesn't exist, the next prior version is rendered ensuring backwards compatibility.
We forked this railscast project and re-implemented it using the versioncake gem. Any comments would be appreciated!
https://github.com/bwillis/350-rest-api-versioning
I have an older project using Rails 2.3.14 and Ruby 1.8.7. Some of the routing constructs used here don't appear to be available in Rails 2. Any suggestions for how to implement a similar versioning system in Rails 2?
Hi Ryan, nice episode.
Is it possible to speed up APIs? In episode #348 you described The Rails API gem and, if I got the concept, many rails features aren't loaded in order to have faster APIs. Is it possible in some way to exclude libraries only for controllers related to APIs?
Thanks in advance.
Hi, nice episode, very helpful.
But (there is always a but lol) i have a problem ! (2 in fact)
I'm using ruby 3.2.11
if I try to use in my routes.rb :
I have an error message 'No route matches [GET] "/dev/v1/media"', if I use a namespace instead (but without constraints) it works! Any idea ? (my first namespace is dev not api)
Other problem, I specified the json format by default in my namespace, set the respond_to :json in my controller but if I use respond_with, got a error message "Missing Template", I need to use :
Any idea ?
Thanks :)
I used rabl in my app. how can I avoid creating controllers which is essentially same as my non-API controllers
RABL works well with versioncake and it allows you to avoid duplicating your controller logic-it's mentioned in the related library section: https://github.com/nesquena/rabl#related-libraries
https://github.com/bwillis/versioncake
One thing to mention is that in the routes file, the default version has to be the last one to be defined.
I had a problem where I added a v2 on top of v1 and made it the default like this:
All V1 requests would be redirected to the V2 Controllers
Thank you!! I had the same problem.
how do you guys test that ApiConstrant ?
and added a new product from curl?
Overriding model in api controller was an excellent piece of code. But can you guide me how and where to put this class in a separate file and include it in api controller.
Hi all
Let's suppose we have an existing rails app (just a basic scaffold - with it's own controllers).
Does this mean, if I am building an API, that I have to write another controller - an API controller?
Would be interested to hear your thoughts. chrs
Ben