Versão da API para rotas Rails


141

Estou tentando versão minha API como o Stripe. Abaixo é fornecida a versão mais recente da API é 2.

/api/users retorna um 301 para /api/v2/users

/api/v1/users retorna um índice de 200 usuários na versão 1

/api/v3/users retorna um 301 para /api/v2/users

/api/asdf/users retorna um 301 para /api/v2/users

Portanto, basicamente, qualquer coisa que não especifique a versão está vinculada à versão mais recente, a menos que exista a versão especificada e redirecione para ela.

Isto é o que eu tenho até agora:

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

Respostas:


280

A forma original desta resposta é muito diferente e pode ser encontrada aqui . Apenas prova de que há mais de uma maneira de esfolar um gato.

Atualizei a resposta desde então para usar namespaces e redirecionamentos 301 - em vez do padrão 302. Agradecemos a pixeltrix e Bo Jeanes pela solicitação sobre essas coisas.


Você pode usar um capacete muito forte, porque isso vai surpreender sua mente .

A API de roteamento do Rails 3 é super maliciosa. Para escrever as rotas para sua API, conforme seus requisitos acima, você precisa apenas disso:

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

Se sua mente ainda estiver intacta após esse ponto, deixe-me explicar.

Primeiro, chamamos de namespaceque é super útil quando você deseja um monte de rotas com escopo definido para um caminho e módulo específicos com nomes semelhantes. Nesse caso, queremos que todas as rotas dentro do bloco namespacetenham o escopo definido para os controladores dentro do Apimódulo e todas as solicitações de caminhos dentro dessa rota serão prefixadas api. Pedidos como /api/v2/users, você sabe?

Dentro do espaço para nome, definimos mais dois espaços para nome (woah!). Desta vez estamos definindo o namespace "v1", de modo que todas as rotas para os controladores aqui vai estar dentro do V1módulo dentro do Apimódulo: Api::V1. Ao definir resources :usersdentro dessa rota, o controlador estará localizado em Api::V1::UsersController. Esta é a versão 1, e você chega lá fazendo solicitações como /api/v1/users.

A versão 2 é apenas uma pequena pouco diferente. Em vez de o controlador atender Api::V1::UsersController, ele está agora Api::V2::UsersController. Você chega lá fazendo pedidos como /api/v2/users.

Em seguida, a matché usado. Isso corresponderá a todas as rotas da API que vão para coisas como /api/v3/users.

Esta é a parte que eu tive que procurar. A :to =>opção permite que você especifique que uma solicitação específica deve ser redirecionada para outro lugar - eu sabia disso -, mas não sabia como redirecioná-la para outro lugar e passar uma parte da solicitação original junto com ela .

Para fazer isso, chamamos o redirectmétodo e passamos uma string com um %{path}parâmetro interpolado especial . Quando chega uma solicitação que corresponde a esta final match, ele interpola o pathparâmetro no local %{path}dentro da string e redireciona o usuário para onde eles precisam ir.

Finalmente, usamos outro matchpara rotear todos os caminhos restantes prefixados /apie redirecioná-los para /api/v2/%{path}. Isso significa que solicitações como /api/usersirão para /api/v2/users.

Eu não conseguia descobrir como obter /api/asdf/userspara corresponder, porque como é possível determinar se isso é suposto ser um pedido para /api/<resource>/<identifier>ou /api/<version>/<resource>?

Enfim, foi divertido pesquisar e espero que ajude você!


24
Caro Ryan Bigg. Você é brilhante.
maletor

18
Não se mede simplesmente a reputação de um Herói Ruby.
Waseem

1
Ryan ... Eu não acho que isso seja realmente preciso. Isso faria com que / api e / api / v2 exibissem o mesmo conteúdo em vez de ter um único URL canônico. / api deve redirecionar para / api / v2 (conforme especificado pelo autor original). Eu esperaria que as rotas corretas se parecessem com gist.github.com/2044335 (concedido, eu ainda não testei isso). Apenas / api / v [12] deve retornar um 200, / api e / api / <ruim versão> deve retornar 301s para / api / v2
Bo Jeanes

2
Vale ressaltar que no arquivo de rotas 301 foi feito o redirecionamento padrão e por boas razões. Dos guias: 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 15/03/12

3
Não cria redirecionamentos infinitos se o caminho não estiver correto? Por exemplo, solicitar / api / v3 / path_that_dont_match_the_routes criará um redirecionamento infinito, certo?
29612 Robin

38

Algumas coisas a acrescentar:

Sua correspondência de redirecionamento não funcionará em determinadas rotas - o *apiparâmetro é ganancioso e engole tudo, por exemplo /api/asdf/users/1, redireciona para /api/v2/1. Você seria melhor usando um parâmetro regular :api. É certo que não corresponderá a casos como o /api/asdf/asdf/users/1caso, mas se você tiver recursos aninhados na sua API, é uma solução melhor.

Ryan POR QUE VOCÊ NÃO GOSTA namespace? :-), por exemplo:

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 possui o benefício adicional de rotas nomeadas com versão e genéricas. Uma observação adicional - a convenção ao usar :moduleé usar notação de sublinhado, por exemplo: api/v1não 'Api :: V1'. Em um ponto, o último não funcionou, mas acredito que foi corrigido no Rails 3.1.

Além disso, quando você libera a v3 da sua API, as rotas são atualizadas da seguinte maneira:

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

Obviamente, é provável que sua API tenha rotas diferentes entre versões. Nesse caso, você pode fazer isso:

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

Como você lidaria com o caso final? ou seja /api/asdf/users?, bem como /api/users/1? Eu não poderia descobrir isso na minha resposta atualizado, por isso imaginei que você poderia saber de uma maneira
Ryan Bigg

Não é uma maneira fácil de fazer isso - você teria que definir todos os redirecionamentos antes da captura, mas só precisaria fazer cada um para cada recurso pai, por exemplo, / api / users / * path => / api / v2 / users /% {path}
pixeltrix 20/03/12

13

Se possível, sugiro repensar seus URLs para que a versão não esteja no URL, mas seja colocada no cabeçalho Accept. Essa resposta de estouro de pilha é muito útil:

Práticas recomendadas para controle de versão da API?

e este link mostra exatamente como fazer isso com o roteamento de trilhos:

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


Essa é uma excelente maneira de fazer isso também e provavelmente atenderia à solicitação "/ api / asdf / users".
Ryan Bigg

9

Eu não sou um grande fã de versionamento por rotas. Criamos o VersionCake para oferecer suporte a uma forma mais fácil de versionamento da API.

Ao incluir o número da versão da API no nome do arquivo de cada uma de suas respectivas visualizações (jbuilder, RABL, etc), mantemos a versão discreta e permitimos degradação fácil para suportar a compatibilidade com versões anteriores (por exemplo, se a v5 da visualização não existir, nós renderizar v4 da visualização).


8

Não sei por que você deseja redirecionar para uma versão específica se uma versão não for solicitada explicitamente. Parece que você simplesmente deseja definir uma versão padrão que será exibida se nenhuma versão for solicitada explicitamente. Também concordo com David Bock que manter as versões fora da estrutura da URL é uma maneira mais limpa de oferecer suporte a versões.

Plugue descarado: o versionista suporta esses casos de uso (e mais).

https://github.com/bploetz/versionist


2

A resposta de Ryan Bigg funcionou para mim.

Se você também deseja manter os parâmetros de consulta através do redirecionamento, pode fazê-lo assim:

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

2

Implementou isso hoje e encontrou o que eu acredito ser o 'caminho certo' no RailsCasts - REST API Versioning . Tão simples. Tão sustentável. Tão eficaz.

Adicionar lib/api_constraints.rb(nem precisa alterar 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

Configure config/routes.rbassim

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 seu controlador (ie /controllers/api/v1/squads_controller.rb)

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

Em seguida, você pode alterar todos os links em seu aplicativo de /api/v1/squadspara /api/squadse você pode facilmente implementar novas versões de API sem precisar alterar os links

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.