Gestion des versions d'API pour les routes Rails


141

J'essaye de mettre à jour mon API comme Stripe. Vous trouverez ci-dessous la dernière version de l'API 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

Donc, fondamentalement, tout ce qui ne spécifie pas la version est lié à la dernière, sauf si la version spécifiée existe, puis redirige vers elle.

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

Réponses:


280

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

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


Vous voudrez peut-être porter un casque vraiment solide, car cela va vous épater .

L'API de routage Rails 3 est super méchante. Pour écrire les routes de 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 toujours intact après ce point, laissez-moi vous expliquer.

Tout d'abord, nous appelons namespacece qui est très pratique lorsque vous voulez un tas de routes étendues à un chemin et un module spécifiques portant le même nom. Dans ce cas, nous voulons que toutes les routes à l'intérieur du bloc pour notre namespacesoient étendues aux contrôleurs dans le Apimodule et toutes les demandes aux chemins à l'intérieur de cette route seront préfixées avec 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 », donc tous les itinéraires pour les contrôleurs ici seront à l' intérieur du V1module à l' intérieur du Apimodule de : Api::V1. En définissant à l' resources :usersinté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 est seulement un petit différent bits. Au lieu que le contrôleur le servant soit à Api::V1::UsersController, il est maintenant à Api::V2::UsersController. Vous y arrivez en faisant des demandes comme /api/v2/users.

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

C'est la partie que je devais rechercher. L' :to =>option vous permet de spécifier qu'une demande spécifique doit être redirigée ailleurs - je le savais bien - mais je ne savais pas comment la faire rediriger vers un autre endroit et transmettre un morceau de la demande d'origine avec elle .

Pour ce faire, nous appelons la redirectméthode et lui passons une chaîne avec un %{path}paramètre interpolé spécial . Lorsqu'une requête correspond à cette finale match, elle interpole le pathparamètre à l'emplacement de l' %{path}intérieur de la chaîne et redirige l'utilisateur vers où il doit aller.

Enfin, nous en utilisons un autre matchpour acheminer tous les chemins restants préfixés par /apiet les rediriger vers /api/v2/%{path}. Cela signifie que les demandes comme /api/usersiront à /api/v2/users.

Je ne savais pas comment faire /api/asdf/userscorrespondre, car comment déterminer si cela est censé être une demande à /api/<resource>/<identifier>ou /api/<version>/<resource>?

Quoi qu'il en soit, c'était amusant de rechercher et j'espère que cela vous aidera!


24
Cher Ryan Bigg. Tu es brillant.
maletor

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

1
Ryan ... Je ne pense pas que ce soit vraiment exact. Cela aurait / api et / api / v2 servir le même contenu au lieu d'avoir une seule URL canonique. / api doit 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 (d'accord, je n'ai pas testé cela, cependant). Seul / api / v [12] devrait renvoyer un 200, / api et / api / <mauvaise version> devrait renvoyer 301s à / api / v2
Bo Jeanes

2
Il convient de noter que dans le fichier de routes 301 a été défini comme redirection par défaut et pour une bonne raison. Des 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

3
Ne crée-t-il pas des redirections infinies si le chemin n'est pas correct? Par exemple, demander / api / v3 / path_that_dont_match_the_routes créera une redirection infinie, non?
Robin

38

Quelques éléments à ajouter:

Votre correspondance de redirection ne fonctionnera pas pour certaines routes - le *apiparamètre est gourmand et engloutira tout, par exemple /api/asdf/users/1, redirigera 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/1mais si vous avez des ressources imbriquées dans votre API, c'est une meilleure solution.

Ryan POURQUOI 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

Ce qui a l'avantage supplémentaire des routes nommées versionnées et génériques. Une note supplémentaire - la convention lors de l'utilisation :moduleest d'utiliser la notation de soulignement, par exemple: api/v1pas 'Api :: V1'. À un moment donné, ce dernier n'a pas fonctionné mais je pense qu'il a été corrigé dans Rails 3.1.

De plus, lorsque vous publiez la v3 de votre API, les routes seraient mises à jour comme ceci:

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 itinéraires différents 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

Comment géreriez-vous le dernier cas? c'est /api/asdf/users?à dire ainsi que /api/users/1? Je ne pouvais pas comprendre cela dans ma réponse mise à jour, alors j'ai pensé que vous pourriez connaître un moyen
Ryan Bigg

Pas de moyen facile de le faire - vous devrez définir toutes les redirections avant le catch all mais vous n'aurez besoin de les faire que pour chaque ressource parent, par exemple / api / users / * path => / api / v2 / users /% {path}
pixeltrix

13

Dans la mesure du possible, je suggérerais de repenser vos URL afin que la version ne soit pas dans l'url, mais soit placée dans l'en-tête accepte. Cette réponse de débordement de pile y va bien:

Bonnes pratiques pour la gestion des versions d'API?

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

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


C'est une excellente façon de le faire aussi, et répondrait probablement aussi à la demande "/ api / asdf / users".
Ryan Bigg

9

Je ne suis pas un grand fan du versionnage par routes. Nous avons créé 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 facile pour prendre en charge la rétrocompatibilité (par exemple, si la version 5 de la vue n'existe pas, nous rendu v4 de la vue).


8

Je ne sais pas pourquoi vous souhaitez rediriger vers une version spécifique si une version n'est pas explicitement demandée. Il semble que vous souhaitiez 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 hors de la structure de l'URL est un moyen plus propre de prendre en charge la gestion des versions.

Plug sans vergogne: Versionist prend en charge ces cas d'utilisation (et plus).

https://github.com/bploetz/versionist


2

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

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

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

2

Je l'ai implémenté aujourd'hui et j'ai trouvé ce que je pense être la «bonne façon» sur RailsCasts - Versioning de l'API REST . Si simple. Tellement maintenable. Tellement efficace.

Ajoutez lib/api_constraints.rb(vous n'avez même pas besoin 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

Configuration config/routes.rbcomme ça

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'

Modifiez 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 de /api/v1/squadsà /api/squadset vous pouvez facilement implémenter de nouvelles versions d'API sans même avoir à modifier les liens

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.