Versiones API para rutas Rails

141

Estoy tratando de versionar mi API como lo ha hecho Stripe. A continuación se muestra la última versión de la API es la 2.

/api/users devuelve un 301 a /api/v2/users

/api/v1/users devuelve un índice de 200 usuarios en la versión 1

/api/v3/users devuelve un 301 a /api/v2/users

/api/asdf/users devuelve un 301 a /api/v2/users

Entonces, básicamente, cualquier cosa que no especifique la versión se vincula a la última, a menos que exista la versión especificada, entonces se redirige a ella.

Esto es lo que tengo hasta ahora:

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
fuente

Respuestas:

280

La forma original de esta respuesta es muy diferente, y se puede encontrar aquí . Solo prueba de que hay más de una forma de pelar un gato.

He actualizado la respuesta desde entonces para usar espacios de nombres y usar redireccionamientos 301, en lugar del valor predeterminado de 302. Gracias a pixeltrix y Bo Jeanes por sus comentarios.


Es posible que desee usar un casco realmente fuerte porque esto le dejará boquiabierto .

La API de enrutamiento Rails 3 es súper malvada. Para escribir las rutas para su API, según sus requisitos anteriores, solo necesita esto:

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 tu mente sigue intacta después de este punto, déjame explicarte.

Primero, llamamos namespacecuál es súper útil para cuando quieres un montón de rutas con alcance a una ruta específica y módulo que tengan un nombre similar. En este caso, queremos que todas las rutas dentro del bloque namespacetengan alcance para los controladores dentro del Apimódulo y todas las solicitudes a rutas dentro de esta ruta tendrán el prefijo api. Solicitudes como /api/v2/users, ¿sabes?

Dentro del espacio de nombres, definimos dos espacios de nombres más (¡woah!). Esta vez estamos definiendo el espacio de nombres "v1", por lo que todas las rutas para los controladores de aquí estarán dentro del V1módulo en el interior del Apimódulo: Api::V1. Al definir resources :usersdentro de esta ruta, el controlador se ubicará en Api::V1::UsersController. Esta es la versión 1, y llega allí haciendo solicitudes como /api/v1/users.

La versión 2 es sólo una pequeña poco diferente. En lugar de que el controlador esté sirviendo Api::V1::UsersController, ahora está en Api::V2::UsersController. Llegas allí haciendo solicitudes como /api/v2/users.

A continuación, matchse utiliza a. Esto coincidirá con todas las rutas API que van a cosas como /api/v3/users.

Esta es la parte que tuve que buscar. La :to =>opción le permite especificar que una solicitud específica se debe redirigir a otro lugar, lo sabía, pero no sabía cómo hacer que se redirigiera a otro lugar y pasar una parte de la solicitud original junto con ella. .

Para hacer esto, llamamos al redirectmétodo y le pasamos una cadena con un %{path}parámetro interpolado especial . Cuando llega una solicitud que coincide con esta final match, interpolará el pathparámetro en la ubicación %{path}dentro de la cadena y redirigirá al usuario a donde debe ir.

Finalmente, usamos otro matchpara enrutar todos los caminos restantes con el prefijo /apiy redirigirlos a ellos /api/v2/%{path}. Esto significa que las solicitudes como /api/usersirán a /api/v2/users.

No pude averiguar cómo llegar /api/asdf/usersa una coincidencia, porque ¿cómo se determina si se supone que es una solicitud /api/<resource>/<identifier>o /api/<version>/<resource>?

De todos modos, esto fue divertido de investigar y espero que te ayude.

Ryan Bigg
fuente
24
Estimado Ryan Bigg. Eres brillante.
maletor
18
Uno no mide simplemente la reputación de un héroe rubí.
Waseem
1
Ryan ... No creo que esto sea realmente correcto. Esto haría que / api y / api / v2 sirvan los mismos contenidos en lugar de tener una única URL canónica. / api debe redirigir a / api / v2 (como lo especificó el autor original). Esperaría que las rutas correctas se parezcan a gist.github.com/2044335 (concedido, sin embargo, no lo he probado). Solo / api / v [12] debería devolver un 200, / api y / api / <versión incorrecta> debería devolver 301s a / api / v2
Bo Jeanes
2
Vale la pena señalar que en el archivo de rutas 301 se ha realizado la redirección predeterminada y por una buena razón. De los guías: 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
¿No crea redirecciones infinitas si la ruta no es correcta? Por ejemplo, solicitar / api / v3 / path_that_dont_match_the_routes creará una redirección infinita, ¿verdad?
Robin
38

Un par de cosas para agregar:

Su coincidencia de redireccionamiento no funcionará para ciertas rutas: el *apiparámetro es codicioso y se tragará todo, por ejemplo /api/asdf/users/1, redireccionará a /api/v2/1. Sería mejor usar un parámetro regular como :api. Es cierto que no coincidirá con casos como, /api/asdf/asdf/users/1pero si tiene recursos anidados en su API, es una mejor solución.

Ryan ¿POR QUÉ NO TE GUSTA namespace? :-), p.ej:

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

Que tiene el beneficio adicional de las rutas nombradas versionadas y genéricas. Una nota adicional: la convención cuando se usa :modulees usar notación de subrayado, por ejemplo: api/v1no 'Api :: V1'. En un momento, este último no funcionó, pero creo que se solucionó en Rails 3.1.

Además, cuando lance la versión 3 de su API, las rutas se actualizarán así:

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

Por supuesto, es probable que su API tenga diferentes rutas entre versiones, en cuyo caso puede hacer esto:

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
fuente
¿Cómo lidiarías con el caso final? es decir /api/asdf/users?, así como /api/users/1? No pude entender eso en mi respuesta actualizada, así que pensé que sabrías de alguna manera
Ryan Bigg
No hay una manera fácil de hacerlo: tendría que definir todas las redirecciones antes de la captura de todo, pero solo necesitaría hacer cada una para cada recurso principal, por ejemplo / api / users / * path => / api / v2 / users /% {path}
pixeltrix
13

Si es posible, sugeriría repensar sus URL para que la versión no esté en la URL, sino que se coloque en el encabezado acepta. Esta respuesta de desbordamiento de pila va bien:

¿Mejores prácticas para el control de versiones de API?

y este enlace muestra exactamente cómo hacerlo con el enrutamiento de rieles:

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

David Bock
fuente
Esta es una excelente manera de hacerlo también, y probablemente también atenderá la solicitud "/ api / asdf / users".
Ryan Bigg
9

No soy un gran fanático del versionado por rutas. Creamos VersionCake para admitir una forma más fácil de versiones de API.

Al incluir el número de versión de API en el nombre de archivo de cada una de nuestras vistas respectivas (jbuilder, RABL, etc.), mantenemos la versión discreta y permitimos una fácil degradación para admitir la compatibilidad con versiones anteriores (por ejemplo, si la v5 de la vista no existe, nosotros render v4 de la vista).

aantix
fuente
8

No estoy seguro de por qué desea redirigir a una versión específica si no se solicita explícitamente una versión. Parece que simplemente desea definir una versión predeterminada que se sirve si no se solicita explícitamente ninguna versión. También estoy de acuerdo con David Bock en que mantener las versiones fuera de la estructura de URL es una forma más limpia de admitir el control de versiones.

Plug descarado: Versionist admite estos casos de uso (y más).

https://github.com/bploetz/versionist

Brian Ploetz
fuente
2

La respuesta de Ryan Bigg funcionó para mí.

Si también desea mantener los parámetros de consulta a través de la redirección, puede hacerlo así:

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

Implementé esto hoy y encontré lo que creo que es la "forma correcta" en RailsCasts - REST API Versioning . Tan sencillo. Tan mantenible. Tan efectivo

Agregar lib/api_constraints.rb(ni siquiera tiene que cambiar 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

Configurar config/routes.rbasí

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'

Edite su controlador (es decir /controllers/api/v1/squads_controller.rb)

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

Luego puede cambiar todos los enlaces de su aplicación de /api/v1/squadsa /api/squadse implementar FÁCILMENTE nuevas versiones de API sin siquiera tener que cambiar los enlaces

weteamsteve
fuente