web-dev-qa-db-fra.com

Versioning API pour Rails Routes

J'essaie de mettre à niveau mon API comme Stripe. Ci-dessous, la dernière version de l’API est la version 2.

/api/users renvoie un 301 à /api/v2/users

/api/v1/users renvoie un index de 200 utilisateurs en version 1

/api/v3/users renvoie un 301 à /api/v2/users

/api/asdf/users renvoie un 301 à /api/v2/users

Donc, si tout ce qui ne spécifie pas la version est lié à la version la plus récente, à moins que la version spécifiée n'existe, redirigez-la.

Voici ce que j'ai jusqu'à présent:

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
140
maletor

La forme originale de cette réponse est extrêmement différente et peut être trouvée ici . Preuve qu'il existe plus d'une façon de peler un chat.

Depuis, j'ai mis à jour la réponse pour utiliser des espaces de noms et des redirections 301 - plutôt que la valeur par défaut de 302. Merci à pixeltrix et Bo Jeanes pour les précisions à ce sujet.


Vous voudrez peut-être porter un casque vraiment fort parce que cela va vous étonner .

L'API de routage Rails 3 est super méchante. Pour écrire les routes pour votre API, conformément à vos exigences ci-dessus, il vous suffit de ceci:

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

Si votre esprit est toujours intact après ce point, laissez-moi vous expliquer.

Tout d’abord, nous appelons namespace, ce qui est très pratique lorsque vous souhaitez regrouper plusieurs itinéraires menant à un chemin et à un module spécifiques portant le même nom. Dans ce cas, nous voulons que toutes les routes du bloc namespace soient étendues aux contrôleurs du module Api et que toutes les demandes d'accès aux chemins de cette route soient précédées de api. Des demandes telles que /api/v2/users, tu sais?

Dans l'espace de noms, nous définissons deux autres espaces de noms (woah!). Cette fois, nous définissons l’espace de noms "v1", de sorte que toutes les routes des contrôleurs ici seront dans le module V1 dans le module Api: Api::V1. En définissant resources :users à l'intérieur de cette route, le contrôleur sera situé à Api::V1::UsersController. C'est la version 1, et vous y arrivez en faisant des requêtes comme /api/v1/users.

La version 2 n’est qu’un peu minuscule. Au lieu que le contrôleur le servant soit à Api::V1::UsersController, il est maintenant à Api::V2::UsersController. Vous y arrivez en faisant des requêtes comme /api/v2/users.

Ensuite, un match est utilisé. Cela correspondra à toutes les routes d'API qui vont à des choses comme /api/v3/users.

C'est la partie que j'ai dû chercher. L'option :to => vous permet de spécifier qu'une requête spécifique doit être redirigée ailleurs - je le savais déjà - mais je ne savais pas comment la faire rediriger vers une autre destination et y passer un élément de la demande originale avec elle.

Pour ce faire, nous appelons la méthode redirect et lui passons une chaîne avec un paramètre %{path} spécial-interpolé. Lorsqu'une demande arrive qui correspond à cette dernière match, il interpolera le paramètre path dans l'emplacement de %{path} à l'intérieur de la chaîne et redirigera l'utilisateur vers l'endroit où il doit aller.

Enfin, nous utilisons un autre match pour router tous les chemins restants avec le préfixe /api et les rediriger vers /api/v2/%{path}. Cela signifie que les demandes comme /api/users iront à /api/v2/users.

Je n'arrivais pas à comprendre comment faire correspondre /api/asdf/users, car comment déterminer si cela est supposé être une demande adressée à /api/<resource>/<identifier> ou /api/<version>/<resource>?

Quoi qu’il en soit, c’était amusant de faire de la recherche et j’espère que cela vous aidera!

275
Ryan Bigg

Quelques choses à ajouter:

Votre correspondance de redirection ne fonctionnera pas pour certaines routes - la *api param est gourmand et engloutit tout, par ex. /api/asdf/users/1 va rediriger vers /api/v2/1. Vous feriez mieux d'utiliser un paramètre régulier comme :api. Certes, cela ne correspondra pas à des cas comme /api/asdf/asdf/users/1 mais si vous avez des ressources imbriquées dans votre API, c'est une meilleure solution.

Ryan POURQUOI VOUS N'AIME PAS namespace? :-), par exemple:

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

Qui a l’avantage supplémentaire des itinéraires nommés versionnés et génériques. Une note supplémentaire - la convention d'utilisation de :module consiste à utiliser la notation de soulignement, par exemple: api/v1 pas 'Api :: V1'. À un moment donné, ce dernier ne fonctionnait pas mais je crois que cela a été corrigé dans Rails 3.1.

En outre, lorsque vous publiez la v3 de votre API, les itinéraires sont mis à jour comme suit:

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

Bien sûr, il est probable que votre API utilise des routes différentes entre les versions, auquel cas vous pouvez le faire:

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
37
pixeltrix

Si possible, je suggérerais de repenser vos URL pour que la version ne soit pas dans l'URL, mais soit placée dans l'en-tête accept. Ce dépassement de pile répond bien:

Meilleures pratiques pour la gestion des versions d'API?

et ce lien montre exactement comment faire cela avec Rails routage:

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

13
David Bock

Je ne suis pas un grand fan de la gestion des versions par routes. Nous avons construit VersionCake pour prendre en charge une forme plus simple de gestion des versions d'API.

En incluant le numéro de version de l’API dans le nom de fichier de chacune de nos vues respectives (jbuilder, RABL, etc.), nous gardons le contrôle de version discret et permettons une dégradation aisée pour prendre en charge la rétrocompatibilité (par exemple, si la v5 de la vue n’existe pas, rendre v4 de la vue).

9
aantix

Je ne sais pas pourquoi vous voulez rediriger vers une version spécifique si une version n'est pas explicitement demandée. On dirait que vous voulez simplement définir une version par défaut qui sera servie si aucune version n'est explicitement demandée. Je conviens également avec David Bock que le fait de garder les versions en dehors de la structure de l'URL est une manière plus simple de prendre en charge le versioning.

Prise sans vergogne: Le versionniste prend en charge ces cas d'utilisation (et plus).

https://github.com/bploetz/versionist

8
Brian Ploetz

Mis en œuvre aujourd'hui et trouvé ce que je crois être la "bonne manière" sur RailsCasts - REST Versioning API) . Si simple. Si maintenable. Si efficace.

Ajouter lib/api_constraints.rb _ (il n'est même pas nécessaire de changer 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

Installer config/routes.rb ainsi

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'

Editez votre contrôleur (ie /controllers/api/v1/squads_controller.rb)

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

Ensuite, vous pouvez modifier tous les liens de votre application à partir de /api/v1/squads à /api/squads et vous pouvez [~ # ~] facilement [~ # ~] implémenter de nouvelles versions d’API sans même avoir à changer de lien

1
weteamsteve

La réponse de Ryan Bigg a fonctionné pour moi.

Si vous souhaitez également conserver les paramètres de requête via la redirection, procédez comme suit:

match "*path", to: redirect{ |params, request| "/api/v2/#{params[:path]}?#{request.query_string}" }
0
Amed Rodríguez