API Versioning for Rails Routes

35,404

Solution 1

The original form of this answer is wildly different, and can be found here. Just proof that there's more than one way to skin a cat.

I've updated the answer since to use namespaces and to use 301 redirects -- rather than the default of 302. Thanks to pixeltrix and Bo Jeanes for the prompting on those things.


You might want to wear a really strong helmet because this is going to blow your mind.

The Rails 3 routing API is super wicked. To write the routes for your API, as per your requirements above, you need just this:

namespace :api do
  namespace :v1 do
    resources :users
  end

  namespace :v2 do
    resources :users
  end
  match 'v:api/*path', :to => redirect("/api/v2/%{path}")
  match '*path', :to => redirect("/api/v2/%{path}")
end

If your mind is still intact after this point, let me explain.

First, we call namespace which is super handy for when you want a bunch of routes scoped to a specific path and module that are similarly named. In this case, we want all routes inside the block for our namespace to be scoped to controllers within the Api module and all requests to paths inside this route will be prefixed with api. Requests such as /api/v2/users, ya know?

Inside the namespace, we define two more namespaces (woah!). This time we're defining the "v1" namespace, so all routes for the controllers here will be inside the V1 module inside the Api module: Api::V1. By defining resources :users inside this route, the controller will be located at Api::V1::UsersController. This is version 1, and you get there by making requests like /api/v1/users.

Version 2 is only a tiny bit different. Instead of the controller serving it being at Api::V1::UsersController, it's now at Api::V2::UsersController. You get there by making requests like /api/v2/users.

Next, a match is used. This will match all API routes that go to things like /api/v3/users.

This is the part I had to look up. The :to => option allows you to specify that a specific request should be redirected somewhere else -- I knew that much -- but I didn't know how to get it to redirect to somewhere else and pass in a piece of the original request along with it.

To do this, we call the redirect method and pass it a string with a special-interpolated %{path} parameter. When a request comes in that matches this final match, it will interpolate the path parameter into the location of %{path} inside the string and redirect the user to where they need to go.

Finally, we use another match to route all remaining paths prefixed with /api and redirect them to /api/v2/%{path}. This means requests like /api/users will go to /api/v2/users.

I couldn't figure out how to get /api/asdf/users to match, because how do you determine if that is supposed to be a request to /api/<resource>/<identifier> or /api/<version>/<resource>?

Anyway, this was fun to research and I hope it helps you!

Solution 2

A couple of things to add:

Your redirect match isn't going to work for certain routes - the *api param is greedy and will swallow up everything, e.g. /api/asdf/users/1 will redirect to /api/v2/1. You'd be better off using a regular param like :api. Admittedly it won't match cases like /api/asdf/asdf/users/1 but if you have nested resources in your api it's a better solution.

Ryan WHY U NO LIKE namespace? :-), e.g:

current_api_routes = lambda do
  resources :users
end

namespace :api do
  scope :module => :v2, &current_api_routes
  namespace :v2, &current_api_routes
  namespace :v1, &current_api_routes
  match ":api/*path", :to => redirect("/api/v2/%{path}")
end

Which has the added benefit of versioned and generic named routes. One additional note - the convention when using :module is to use underscore notation, e.g: api/v1 not 'Api::V1'. At one point the latter didn't work but I believe it was fixed in Rails 3.1.

Also, when you release v3 of your API the routes would be updated like this:

current_api_routes = lambda do
  resources :users
end

namespace :api do
  scope :module => :v3, &current_api_routes
  namespace :v3, &current_api_routes
  namespace :v2, &current_api_routes
  namespace :v1, &current_api_routes
  match ":api/*path", :to => redirect("/api/v3/%{path}")
end

Of course it's likely that your API has different routes between versions in which case you can do this:

current_api_routes = lambda do
  # Define latest API
end

namespace :api do
  scope :module => :v3, &current_api_routes
  namespace :v3, &current_api_routes

  namespace :v2 do
    # Define API v2 routes
  end

  namespace :v1 do
    # Define API v1 routes
  end

  match ":api/*path", :to => redirect("/api/v3/%{path}")
end

Solution 3

If at all possible, I would suggest rethinking your urls so that the version isn't in the url, but is put into the accepts header. This stack overflow answer goes into it well:

Best practices for API versioning?

and this link shows exactly how to do that with rails routing:

http://freelancing-gods.com/posts/versioning_your_ap_is

Solution 4

I'm not a big fan of versioning by routes. We built VersionCake to support an easier form of API versioning.

By including the API version number in the filename of each of our respective views (jbuilder, RABL, etc), we keep the versioning unobtrusive and allow for easy degradation to support backwards compatibility (e.g. if v5 of the view doesn't exist, we render v4 of the view).

Solution 5

I'm not sure why you want to redirect to a specific version if a version isn't explicitly requested. Seems like you simply want to define a default version that gets served up if no version is explicitly requested. I also agree with David Bock that keeping versions out of the URL structure is a cleaner way to support versioning.

Shameless plug: Versionist supports these use cases (and more).

https://github.com/bploetz/versionist

Share:
35,404
maletor
Author by

maletor

I like bagels, meditation, and music.

Updated on July 08, 2022

Comments

  • maletor
    maletor almost 2 years

    I'm trying to version my API like Stripe has. Below is given the latest API version is 2.

    /api/users returns a 301 to /api/v2/users

    /api/v1/users returns a 200 of users index at version 1

    /api/v3/users returns a 301 to /api/v2/users

    /api/asdf/users returns a 301 to /api/v2/users

    So that basically anything that doesn't specify the version links to the latest unless the specified version exists then redirect to it.

    This is what I have so far:

    scope 'api', :format => :json do
      scope 'v:api_version', :api_version => /[12]/ do
        resources :users
      end
    
      match '/*path', :to => redirect { |params| "/api/v2/#{params[:path]}" }
    end
    
  • maletor
    maletor about 12 years
    Dear Ryan Bigg. You are brilliant.
  • Waseem
    Waseem about 12 years
    One does not simply measure reputation of a Ruby Hero.
  • Bo Jeanes
    Bo Jeanes about 12 years
    Ryan... I don't think this is actually accurate. This would have /api and /api/v2 serve the same contents instead of having a single canonical URL. /api should redirect to /api/v2 (as the original author specified). I'd expect the correct routes to look something like gist.github.com/2044335 (granted, I haven't tested that, though). Only /api/v[12] should return a 200, /api and /api/<bad version> should return 301s to /api/v2
  • maletor
    maletor about 12 years
    It's worth noting that in the routes file 301 has been made the default redirect and for good reason. From the guides: Please note that this redirection is a 301 “Moved Permanently” redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible.
  • maletor
    maletor about 12 years
    If possible it would be nice to be able to return 404 instead of too many redirects as well.
  • Ryan Mohr
    Ryan Mohr about 12 years
    What is the point of the first match? If someone requests the v3 api and v2 is the highest, it would make more sense to render a 404 instead. Also, you can replace match '*path', :to => redirect("/api/v2/%{path}", :status => 301) with the simpler form match '*path' => redirect("/api/v2/%{path}").
  • owl
    owl about 12 years
    How would you deal with the final case? i.e. /api/asdf/users? as well as /api/users/1? I couldn't figure that out in my updated answer, so figured that you might know of a way
  • pixeltrix
    pixeltrix about 12 years
    No easy way to do it - you'd have to define all the redirects before the catch all but you'd only need to do each for each parent resource, e.g. /api/users/*path => /api/v2/users/%{path}
  • owl
    owl about 12 years
    This is an excellent way of doing it also, and would probably cater for the "/api/asdf/users" request as well.
  • Robin
    Robin about 12 years
    Doesn't it create infinite redirects if the path is not correct? For instance, requesting /api/v3/path_that_dont_match_the_routes will create an infinite redirection, right?
  • owl
    owl about 12 years
    @Robin: Most likely. How would you go about fixing it?
  • you786
    you786 about 11 years
    I think this is broken in the most recent version of Rails 3: I tried it an it caused infinite redirects. I posted a question, then my solution here: stackoverflow.com/a/15755981/458968
  • shime
    shime almost 11 years
    Great implementation of versioning. It renders a redirect in HTMl, though, while APIs are written for JSON or XML.
  • owl
    owl almost 11 years
    @shime: Really? The redirect should be handled by other routes in config/routes.rb. Do you have an example app that does this that I can poke + prod at?
  • shime
    shime almost 11 years
    Yup :( Sure. There you go: gist.github.com/shime/6129865 Sorry for the late response, I had to poke with it myself :P
  • Habovh
    Habovh over 8 years
    The last paragraph is definitely what I was looking for... Makes sense now that you cannot name the API version android, ios or angular to manage multiple kind of APIs using versioning...