API-Versionierung für Rails-Routen

141

Ich versuche, meine API wie Stripe zu versionieren. Unten ist die neueste API-Version 2 angegeben.

/api/users gibt eine 301 an zurück /api/v2/users

/api/v1/users Gibt einen 200-Benutzer-Index in Version 1 zurück

/api/v3/users gibt eine 301 an zurück /api/v2/users

/api/asdf/users gibt eine 301 an zurück /api/v2/users

Damit im Grunde alles, was die Version nicht angibt, auf die neueste Version verweist, es sei denn, die angegebene Version ist vorhanden, wird auf diese umgeleitet.

Das habe ich bisher:

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
quelle

Antworten:

280

Die ursprüngliche Form dieser Antwort ist völlig anders und kann hier gefunden werden . Nur ein Beweis dafür, dass es mehr als einen Weg gibt, eine Katze zu häuten.

Ich habe die Antwort seitdem aktualisiert, um Namespaces zu verwenden und 301-Weiterleitungen zu verwenden - anstelle der Standardeinstellung 302. Vielen Dank an Pixeltrix und Bo Jeanes für die Aufforderung zu diesen Dingen.


Vielleicht möchten Sie einen wirklich starken Helm tragen, da dies Sie umhauen wird .

Die Rails 3-Routing-API ist super böse. Um die Routen für Ihre API gemäß Ihren obigen Anforderungen zu schreiben, benötigen Sie genau Folgendes:

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

Wenn Ihr Geist nach diesem Punkt noch intakt ist, lassen Sie mich das erklären.

Zunächst rufen wir an, namespacewas sehr praktisch ist, wenn Sie eine Reihe von Routen für einen bestimmten Pfad und ein bestimmtes Modul mit ähnlichen Namen wünschen. In diesem Fall möchten wir, dass alle Routen innerhalb des Blocks für namespaceController innerhalb des ApiModuls festgelegt werden und allen Anforderungen an Pfade innerhalb dieser Route ein Präfix vorangestellt wird api. Anfragen wie /api/v2/users, weißt du?

Innerhalb des Namespace definieren wir zwei weitere Namespaces (woah!). Dieses Mal definieren wir den Namespace "v1", sodass sich alle Routen für die Controller hier innerhalb des V1Moduls innerhalb des ApiModuls befinden : Api::V1. Durch die Definition resources :usersinnerhalb dieser Route befindet sich der Controller unter Api::V1::UsersController. Dies ist Version 1, und Sie gelangen dorthin, indem Sie Anfragen wie stellen /api/v1/users.

Version 2 ist nur ein kleines bisschen anders. Anstelle des Controllers, der es bedient, Api::V1::UsersControllerist es jetzt bei Api::V2::UsersController. Sie gelangen dorthin, indem Sie Anfragen wie stellen /api/v2/users.

Als nächstes wird a matchverwendet. Dies entspricht allen API-Routen, die zu Dingen wie gehen /api/v3/users.

Dies ist der Teil, den ich nachschlagen musste. Mit dieser :to =>Option können Sie festlegen, dass eine bestimmte Anforderung an eine andere Stelle umgeleitet werden soll - das wusste ich -, aber ich wusste nicht, wie ich sie an eine andere Stelle umleiten und einen Teil der ursprünglichen Anforderung zusammen mit dieser weiterleiten kann .

Dazu rufen wir die redirectMethode auf und übergeben ihr einen String mit einem speziell interpolierten %{path}Parameter. Wenn eine Anforderung eingeht, die diesem Finale entspricht match, interpoliert sie den pathParameter in die Position %{path}innerhalb der Zeichenfolge und leitet den Benutzer dorthin weiter, wo er hin muss.

Schließlich verwenden wir einen anderen match, um alle verbleibenden Pfade, denen das Präfix vorangestellt ist, weiterzuleiten /apiund an diese umzuleiten /api/v2/%{path}. Dies bedeutet, dass Anfragen wie /api/usersan gehen /api/v2/users.

Ich konnte nicht herausfinden, wie ich /api/asdf/userszum Match kommen kann, denn wie bestimmen Sie, ob dies eine Anfrage an /api/<resource>/<identifier>oder sein soll /api/<version>/<resource>?

Wie auch immer, es hat Spaß gemacht zu recherchieren und ich hoffe es hilft dir!

Ryan Bigg
quelle
24
Lieber Ryan Bigg. Du bist Brilliant.
Maletor
18
Man misst nicht einfach den Ruf eines Ruby Hero.
Waseem
1
Ryan ... Ich denke nicht, dass das wirklich richtig ist. Dies hätte zur Folge, dass / api und / api / v2 denselben Inhalt bereitstellen, anstatt eine einzige kanonische URL zu haben. / api sollte nach / api / v2 umleiten (wie vom ursprünglichen Autor angegeben). Ich würde erwarten, dass die richtigen Routen ungefähr so aussehen wie gist.github.com/2044335 ( zugegeben , das habe ich allerdings nicht getestet). Nur / api / v [12] sollte eine 200 zurückgeben, / api und / api / <schlechte Version> sollten 301s an / api / v2 zurückgeben
Bo Jeanes
2
Es ist erwähnenswert, dass in der Routendatei 301 aus gutem Grund die Standardumleitung vorgenommen wurde. Von den Führern: 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
Erstellt es nicht unendliche Weiterleitungen, wenn der Pfad nicht korrekt ist? Wenn Sie beispielsweise / api / v3 / path_that_dont_match_the_routes anfordern, wird eine unendliche Umleitung erstellt, oder?
Robin
38

Ein paar Dinge hinzuzufügen:

Ihr Weiterleitungs-Match funktioniert für bestimmte Routen nicht - der *apiParameter ist gierig und verschluckt alles, z . B. /api/asdf/users/1leitet er weiter /api/v2/1. Sie sollten einen normalen Parameter wie verwenden :api. Zugegeben, es passt nicht zu Fällen wie, /api/asdf/asdf/users/1aber wenn Sie verschachtelte Ressourcen in Ihrer API haben, ist es eine bessere Lösung.

Ryan WARUM MÖGEN SIE NICHT namespace? :-), z.B:

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

Welches den zusätzlichen Vorteil von versionierten und generischen benannten Routen hat. Ein zusätzlicher Hinweis - die Konvention bei der Verwendung :moduleist die Verwendung der Unterstrichnotation, z. B.: api/v1Nicht 'Api :: V1'. Letzteres hat irgendwann nicht funktioniert, aber ich glaube, es wurde in Rails 3.1 behoben.

Wenn Sie Version 3 Ihrer API veröffentlichen, werden die Routen wie folgt aktualisiert:

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

Natürlich ist es wahrscheinlich, dass Ihre API unterschiedliche Routen zwischen den Versionen hat. In diesem Fall können Sie dies tun:

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
Pixeltrix
quelle
Wie würden Sie mit dem letzten Fall umgehen? dh /api/asdf/users?so gut wie /api/users/1? Ich konnte das in meiner aktualisierten Antwort nicht herausfinden, also dachte ich mir, dass Sie vielleicht einen Weg kennen
Ryan Bigg
Kein einfacher Weg, dies zu tun - Sie müssten alle Weiterleitungen definieren, bevor Sie alle abfangen, aber Sie müssten nur jede für jede übergeordnete Ressource ausführen, z. B. / api / users / * path => / api / v2 / users /% {Pfad}
Pixeltrix
13

Wenn möglich, würde ich vorschlagen, Ihre URLs zu überdenken, damit die Version nicht in der URL enthalten ist, sondern in den Accept-Header eingefügt wird. Diese Antwort auf den Stapelüberlauf passt gut dazu:

Best Practices für die API-Versionierung?

und dieser Link zeigt genau, wie das mit Schienenführung gemacht wird:

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

David Bock
quelle
Dies ist auch eine hervorragende Möglichkeit, dies zu tun, und würde wahrscheinlich auch die Anforderung "/ api / asdf / users" berücksichtigen.
Ryan Bigg
9

Ich bin kein großer Fan von Routenversionen. Wir haben VersionCake entwickelt , um eine einfachere Form der API-Versionierung zu unterstützen.

Indem wir die API-Versionsnummer in den Dateinamen jeder unserer jeweiligen Ansichten (jbuilder, RABL usw.) aufnehmen, behalten wir die Versionsverwaltung unauffällig bei und ermöglichen eine einfache Verschlechterung, um die Abwärtskompatibilität zu unterstützen (z. B. wenn v5 der Ansicht nicht vorhanden ist, sind wir es v4 der Ansicht rendern).

Aantix
quelle
8

Ich bin nicht sicher, warum Sie zu einer bestimmten Version umleiten möchten, wenn eine Version nicht explizit angefordert wird. Anscheinend möchten Sie einfach eine Standardversion definieren, die bereitgestellt wird, wenn keine Version explizit angefordert wird. Ich stimme auch David Bock zu, dass das Heraushalten von Versionen aus der URL-Struktur eine sauberere Möglichkeit ist, die Versionierung zu unterstützen.

Schamloser Plug: Versionist unterstützt diese Anwendungsfälle (und mehr).

https://github.com/bploetz/versionist

Brian Ploetz
quelle
2

Die Antwort von Ryan Bigg hat bei mir funktioniert.

Wenn Sie auch Abfrageparameter über die Umleitung beibehalten möchten, können Sie dies folgendermaßen tun:

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

Habe dies heute implementiert und festgestellt, was meiner Meinung nach der richtige Weg für RailsCasts ist - REST API Versioning . So einfach. So wartbar. So effektiv.

Hinzufügen lib/api_constraints.rb(muss nicht einmal vnd.example ändern.)

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

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'

Bearbeiten Sie Ihren Controller (dh /controllers/api/v1/squads_controller.rb)

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

Dann können Sie alle Links in Ihrer App von /api/v1/squadsbis ändern /api/squadsund EINFACH neue API-Versionen implementieren, ohne die Links ändern zu müssen

Weteamsteve
quelle