API Versioning for Rails Routes

前端 未结 7 650
独厮守ぢ
独厮守ぢ 2020-12-04 04:12

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

<
相关标签:
7条回答
  • 2020-12-04 04:49

    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

    0 讨论(0)
  • 2020-12-04 04:59

    Implemented this today and found what I believe to be the 'right way' on RailsCasts - REST API Versioning. So simple. So maintainable. So effective.

    Add lib/api_constraints.rb (don't even have to change vnd.example.)

    class ApiConstraints
      def initialize(options)
        @version = options[:version]
        @default = options[:default]
      end
    
      def matches?(req)
        @default || req.headers['Accept'].include?("application/vnd.example.v#{@version}")
      end
    end
    

    Setup config/routes.rb like so

    require 'api_constraints'
    
    Rails.application.routes.draw do
    
      # Squads API
      namespace :api do
        # ApiConstaints is a lib file to allow default API versions,
        # this will help prevent having to change link names from /api/v1/squads to /api/squads, better maintainability
        scope module: :v1, constraints: ApiConstraints.new(version:1, default: true) do
          resources :squads do
            # my stuff was here
          end
        end
      end
    
      resources :squads
      root to: 'site#index'
    

    Edit your controller (ie /controllers/api/v1/squads_controller.rb)

    module Api
      module V1
        class SquadsController < BaseController
          # my stuff was here
        end
      end
    end
    

    Then you can change all links in your app from /api/v1/squads to /api/squads and you can EASILY implement new api versions without even having to change links

    0 讨论(0)
  • 2020-12-04 05:01

    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
    
    0 讨论(0)
  • 2020-12-04 05:04

    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!

    0 讨论(0)
  • 2020-12-04 05:10

    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).

    0 讨论(0)
  • 2020-12-04 05:10

    Ryan Bigg answer worked for me.

    If you also want to keep query parameters through the redirect, you can do it like this:

    match "*path", to: redirect{ |params, request| "/api/v2/#{params[:path]}?#{request.query_string}" }
    
    0 讨论(0)
提交回复
热议问题