151 votes

Versioning de l'API pour les routes Rails

J'essaie de versionner mon API comme Stripe l'a fait. La dernière version de l'API est la 2.

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

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

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

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

Ainsi, tout ce qui ne spécifie pas la version est lié à la dernière version, à moins que la version spécifiée n'existe et qu'elle soit redirigée vers celle-ci.

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

288voto

Ryan Bigg Points 64561

La forme originale de cette réponse est très différente et peut être trouvée ici. . C'est la preuve qu'il y a plus d'une façon d'écorcher un chat.

J'ai mis à jour la réponse depuis, afin d'utiliser les espaces de noms et les redirections 301 - plutôt que la redirection 302 par défaut. Merci à pixeltrix et à Bo Jeanes pour avoir insisté sur ces points.


Vous pourriez vouloir porter un vraiment un casque solide parce que ça va épatez votre esprit .

L'API de routage de Rails 3 est super géniale. Pour écrire les routes pour votre API, conformément à vos exigences ci-dessus, vous avez juste besoin 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 encore intact après ce point, laissez-moi vous expliquer.

D'abord, nous appelons namespace ce qui est très pratique lorsque l'on veut avoir un tas de routes ayant une portée sur un chemin spécifique et un module qui ont un nom similaire. Dans ce cas, nous voulons que toutes les routes à l'intérieur du bloc pour notre module namespace pour qu'il soit adapté aux contrôleurs dans le cadre de l'initiative Api et toutes les requêtes vers les chemins à l'intérieur de cette route seront préfixées avec api . Des demandes telles que /api/v2/users tu sais ?

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

La version 2 n'est qu'une petit un peu différent. Au lieu que le contrôleur qui le sert soit à Api::V1::UsersController il est maintenant à Api::V2::UsersController . Vous y arrivez en faisant des demandes comme /api/v2/users .

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

C'est la partie que j'ai dû chercher. Le site :to => vous permet de spécifier qu'une requête spécifique doit être redirigée vers un autre endroit - je le savais déjà - mais je ne savais pas comment faire pour qu'elle soit redirigée vers un autre endroit et transmettre un morceau de la requête originale en même temps.

Pour ce faire, nous appelons le redirect et lui transmettre une chaîne de caractères avec une valeur de %{path} paramètre. Lorsqu'une demande arrive et qu'elle correspond à ce paramètre final match il interpolera le path dans l'emplacement de %{path} à l'intérieur de la chaîne et redirige l'utilisateur vers l'endroit où il doit aller.

Enfin, nous utilisons une autre match pour acheminer tous les chemins restants préfixés par /api et les rediriger vers /api/v2/%{path} . Cela signifie que les demandes comme /api/users ira à /api/v2/users .

Je n'arrivais pas à trouver comment obtenir /api/asdf/users de correspondre, car comment déterminer si c'est censé être une demande de /api/<resource>/<identifier> o /api/<version>/<resource> ?

Quoi qu'il en soit, cette recherche a été amusante et j'espère qu'elle vous aidera !

26 votes

Cher Ryan Bigg. Vous êtes brillant.

19 votes

On ne mesure pas simplement la réputation d'un Ruby Hero.

1 votes

Ryan... Je ne pense pas que ce soit vraiment exact. Ainsi, /api et /api/v2 serviraient le même contenu au lieu d'avoir une seule URL canonique. /api devrait rediriger vers /api/v2 (comme l'auteur original l'a spécifié). Je m'attendrais à ce que les routes correctes ressemblent à quelque chose comme gist.github.com/2044335 (je ne l'ai pas encore testé, cependant). Seulement /api/v[12] devrait renvoyer un 200, /api et /api/<mauvaise version> devraient renvoyer des 301s vers /api/v2

38voto

pixeltrix Points 786

Deux ou trois choses à ajouter :

Votre correspondance de redirection ne fonctionnera pas pour certains itinéraires. *api param est avide et avale tout, par ex. /api/asdf/users/1 sera redirigé vers /api/v2/1 . Vous feriez mieux d'utiliser un paramètre régulier comme :api . Il est vrai que cela ne correspond pas aux cas tels que /api/asdf/asdf/users/1 mais si vous avez des ressources imbriquées dans votre api, c'est une meilleure solution.

Ryan pourquoi tu n'aimes 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

Ce qui présente l'avantage supplémentaire d'avoir des itinéraires avec des versions et des noms génériques. Une note supplémentaire - la convention lors de l'utilisation de :module consiste à utiliser la notation par 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.

De même, lorsque vous publierez la version 3 de votre API, les routes seront mises à 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 ait 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

0 votes

Comment traiteriez-vous le cas final ? c'est-à-dire /api/asdf/users? ainsi que /api/users/1 ? Je n'ai pas réussi à trouver la solution dans ma réponse actualisée, alors j'ai pensé que vous connaissiez peut-être un moyen de le faire.

0 votes

Il n'y a pas de moyen simple de le faire - il faut définir toutes les redirections avant le catch all mais il faut le faire pour chaque ressource parent, par exemple /api/users/*path => /api/v2/users/%{path}.

13voto

David Bock Points 318

Dans la mesure du possible, je vous suggère de repenser vos urls de manière à ce que la version ne soit pas dans l'url, mais dans l'en-tête accepts. Cette réponse de stack overflow l'explique bien :

Meilleures pratiques pour le versionnement des API ?

et ce lien montre exactement comment faire cela avec le routage des rails :

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

0 votes

C'est également une excellente façon de procéder, et cela répondrait probablement à la demande "/api/asdf/users".

9voto

aantix Points 359

Je ne suis pas un grand fan du versioning par routes. Nous avons construit VersionCake pour prendre en charge une forme plus simple de versionnement des 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 versionnage discret et permettons une dégradation facile pour soutenir la compatibilité ascendante (par exemple, si la v5 de la vue n'existe pas, nous rendons la v4 de la vue).

8voto

Brian Ploetz Points 243

Je ne suis pas sûr de savoir pourquoi vous voulez rediriger à une version spécifique si une version n'est pas explicitement demandée. Il semble que vous vouliez simplement définir une version par défaut qui sera servie si aucune version n'est explicitement demandée. Je suis également d'accord avec David Bock pour dire que garder les versions en dehors de la structure de l'URL est une façon plus propre de supporter le versioning.

Une publicité éhontée : Versionist supporte ces cas d'utilisation (et plus encore).

https://github.com/bploetz/versionist

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X