Rails para APIs

Rails para APIs

Veja como o Rails pode ser uma alternativa viável e muito melhor para a criação de APIs.

Cb5d9e9095cd41b636764a85e57ade4b?s=128

Nando Vieira

May 18, 2013
Tweet

Transcript

  1. RAILS PARA APIS NANDO VIEIRA @fnando

  2. http://nandovieira.com.br

  3. http://hellobits.com

  4. http://howtocode.com.br howto.

  5. http://codeplane.com.br

  6. None
  7. O Rails mudou o modo como criamos aplicações web.

  8. Não precisamos nos preocupar mais como será a organização dos

    arquivos.
  9. Nem como definir as rotas que nosso app vai reconhecer.

  10. Mas isso tem um custo que para APIs pode não

    ser muito bom.
  11. APIs tem que ser rápidas.

  12. Você tem outras alternativas se quiser continuar com Ruby, como

    Sinatra.
  13. No Sinatra, você volta ao problema de ter que fazer

    tudo o que o Rails já fazia por você.
  14. Rails para API = Rails - Web app stuff

  15. Instalando

  16. $ gem install rails --pre

  17. $ rails new codeplane -TJ -d postgresql

  18. Dependências

  19. Remova tudo o que você não precisa do Gemfile.

  20. ruby "2.0.0" source "https://rubygems.org" gem "rails", "4.0.0.rc1" gem "pg" gem

    "rack-cache", require: "rack/cache" gem "dalli"
  21. Middlewares

  22. O Rails possui uma série de middlewares que não são

    necessários para APIs.
  23. # config/initializers/middleware.rb Rails.configuration.middleware.tap do |middleware| middleware.delete ActionDispatch::Cookies middleware.delete ActionDispatch::Flash middleware.delete

    ActionDispatch::Session::CookieStore middleware.delete ActionDispatch::Static middleware.delete Rack::MethodOverride end
  24. Recebendo parâmetros

  25. Desabilite o CSRF token.

  26. # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Prevent CSRF attacks

    by raising an exception. # For APIs, you may want to use :null_session instead. # protect_from_forgery with: :exception end
  27. No Rails, estamos acostumados a receber hashes como POST data.

  28. User.new(params[:user])

  29. { name: "John Doe", email: "john@example.org", password: "[FILTERED]", password_confirmation: "[FILTERED]"

    }
  30. Isso não é comum em outras linguagens/frameworks.

  31. Receber os parâmetros na raíz pode ser um problema.

  32. post "/:name", to: "application#index"

  33. $ http POST localhost:3002/hello name="John Doe"

  34. params[:name] #=> hello

  35. request.request_parameters

  36. # app/services/body_data.rb class BodyData def initialize(params) @params = params end

    def only(*attrs) @params.reduce({}) do |buffer, (key, value)| buffer[key] = value if attrs.include?(key.to_sym) buffer end end end
  37. # app/controllers/application_controller.rb class ApplicationController < ActionController::Base def body_data @body_data ||=

    BodyData.new(request.request_parameters) end end
  38. repo = Repo.new body_data.only(:name)

  39. Representando dados

  40. Existem inúmeras maneiras de representar os dados em uma API.

  41. JSON JavaScript Object Notation

  42. # app/controllers/repos_controller.rb class ReposController < ApplicationController respond_to :json def index

    repos = RepoList.new(current_user).all respond_with repos end end
  43. Crie uma camada de abstração dos dados que serão representados.

  44. http://rubygems.org/gems/active_model_serializers

  45. # app/models/repo.rb class Repo < ActiveRecord::Base def active_model_serializer RepoSerializer end

    end
  46. # app/serializers/repo_serializer.rb class RepoSerializer < ActiveModel::Serializer attributes :name, :created_at end

  47. http localhost:3002/repos

  48. { "repos": [ { "created_at": "2013-05-18T13:48:11.006Z", "name": "codeplane" }, {

    "created_at": "2013-05-18T13:48:17.109Z", "name": "howto" } ] }
  49. Schemaless é bom, mas é ruim.

  50. http://stateless.co/hal_specification.html

  51. http://jsonapi.org

  52. JSON com significado e Hypermedia permitem criar ferramentas automatizadas.

  53. https://github.com/apotonick/roar

  54. # app/controllers/repos_controller.rb class ReposController < ApplicationController include Roar::Rails::ControllerAdditions respond_to :json

    def index respond_with Repo.all end end
  55. # app/representers/repos_representer.rb module ReposRepresenter include Roar::Representer::JSON::HAL include Roar::Representer::Feature::Hypermedia collection :repos,

    :extend => RepoRepresenter link :self do repos_url end def repos each end end
  56. # app/representers/repo_representer.rb module RepoRepresenter include Roar::Representer::JSON::HAL include Roar::Representer::Feature::Hypermedia property :name

    property :created_at link :self do repo_url(represented) end end
  57. { "_links": { "self": { "href": "http://127.0.0.1:3000/repos" } }, "repos":

    [ { "_links": { "self": { "href": "http://127.0.0.1:3000/repos/1" } }, "created_at": "2013-05-18T13:48:11.006Z", "name": "codeplane" } ] }
  58. O Rails possui suporte às convenções REST desde o Rails

    2.
  59. Mas isso não significa que seu app será Hypermedia.

  60. Não implementa o método HTTP OPTIONS.

  61. # config/initializers/action_dispatch.rb class ActionDispatch::Routing::Mapper module HttpHelpers def options(*args, &block) map_method(:options,

    args, &block) end end end
  62. http http://localhost:3000/

  63. { "_links": { "locales": { "href": "http://localhost:3000/help/locales" }, "self": {

    "href": "http://localhost:3000/" }, "timezones": { "href": "http://localhost:3000/help/timezones" }, "users": { "href": "http://localhost:3000/users" } } }
  64. http http://localhost:3000/help/locales

  65. { "_links": { "self": { "href": "http://localhost:3000/help/locales" } }, "locales":

    [ { "code": "en", "name": "English" }, { "code": "pt-BR", "name": "Brazilian Portuguese" } ] }
  66. http OPTIONS http://localhost:3000/users

  67. HTTP/1.1 200 OK Allow: POST Cache-Control: max-age=0, private, must-revalidate Connection:

    close Content-Encoding: gzip Content-Type: text/html ETag: "bfdba25abe4e2efd6ba82155b8aa0719" Server: thin 1.5.1 codename Straight Razor Vary: Accept-Language,Accept-Encoding,Origin X-Request-Id: ca7031fd-5190-4d9c-87b5-fac42dff926a X-Runtime: 0.036035
  68. # config/routes.rb Codeplane::Application.routes.draw do controller :users do post "/users", action:

    :create options "/users", to: allow(:post), as: false end end
  69. # config/initializers/action_dispatch.rb class ActionDispatch::Routing::Mapper module HttpHelpers def allow(*verbs) AllowedMethods.new(*verbs) end

    end end
  70. # config/middleware/allowed_methods.rb class AllowedMethods attr_reader :verbs def initialize(*verbs) @verbs =

    verbs end def call(env) [ 200, { "Content-Type" => "text/html", "Allow" => verbs.map(&:upcase).join(",") }, [] ] end end
  71. http POST http://localhost:3000/users

  72. { "errors": { "email": [ "is invalid" ], "name": [

    "can't be blank" ], "password": [ "can't be blank" ], "username": [ "is invalid" ] } }
  73. http POST http://localhost:3000/users \ Accept-Language:pt-BR

  74. { "errors": { "email": [ "é inválido" ], "name": [

    "não pode ficar em branco" ], "password": [ "não pode ficar em branco" ], "username": [ "não é válido" ] } }
  75. class Locale attr_reader :app, :i18n def initialize(app, i18n = I18n)

    @app = app @i18n = i18n end def call(env) i18n.locale = env["HTTP_ACCEPT_LANGUAGE"] app.call(env) end end
  76. Autenticação

  77. Aqui você também possui diversas alternativas.

  78. Autenticação por query string, header, OAuth, Basic Auth, e muito

    mais.
  79. Comece simples e mude a autenticação no futuro se for

    necessário.
  80. O Rails possui seu próprio mecanismo de autenticação por token.

  81. authenticate_or_request_with_http_token(realm, &block)

  82. # app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_action :require_authentication attr_reader :current_user

    def require_authentication authenticate_or_request_with_http_token do |token, options| @current_user = Authorizer.authorize(token) end end end
  83. http localhost:3002/repos Authorization:"Token token=abc"

  84. Exija HTTPS.

  85. Codeplane::Application.configure do config.force_ssl = true end

  86. http://www.cheapssls.com

  87. Veracidade

  88. Muitas APIs precisam garantir a veracidade das informações que estão

    sendo enviadas.
  89. Assinatura

  90. secret + timestamp + url + data

  91. require "openssl" # Request time timestamp = Time.now # Requested

    url url = "https://codeplane.com/repos" # API secret secret = "MYSUPERSECRET" # POST data options = {name: "myrepo"} # Hashing algorithm digest = OpenSSL::Digest::Digest.new("sha512")
  92. components = [url, secret, timestamp] components += options .keys .sort

    .reduce(components) do |buffer, key| buffer.concat [key, options[key]] end
  93. signature = OpenSSL::HMAC.hexdigest( digest, secret, components.join("") )

  94. https://gist.github.com/fnando/5885956b831ea2c9b31f

  95. Caching

  96. Além das técnicas de caching usuais da aplicação, você pode

    implementar HTTP caching.
  97. O Rack::Cache permite usar HTTP caching com headers de tempo

    de expiração (Expires, Cache- Control) ou validação (Etag, Last- Modified). https://github.com/rtomayko/rack-cache
  98. # config/initializers/middleware.rb Rails.configuration.middleware.tap do |middleware| # ... middleware.use ::Rack::Cache, :verbose

    => true, :metastore => "memcached://localhost:11211/meta", :entitystore => "memcached://localhost:11211/body" end
  99. http http://localhost:3000/

  100. HTTP/1.1 200 OK Cache-Control: max-age=0, private, must-revalidate Connection: close Content-Encoding:

    gzip Content-Type: application/json; charset=utf-8 ETag: "1358b2b2117fd019f60598f7a5514da2" Server: thin 1.5.1 codename Straight Razor Vary: Accept-Language,Accept-Encoding,Origin X-Request-Id: 4e7b55af-236c-4fcd-ae09-18d88e9019c0 X-Runtime: 0.071640
  101. GZIP

  102. Sirva compressão GZIP para quem entende.

  103. http GET http://localhost:3000/ \ Accept-Encoding:none

  104. http GET http://localhost:3000/ \ Accept-Encoding:gzip

  105. HTTP/1.1 200 OK Cache-Control: max-age=0, private, must-revalidate Connection: close Content-Encoding:

    gzip Content-Type: application/json; charset=utf-8 ETag: "1358b2b2117fd019f60598f7a5514da2" Server: thin 1.5.1 codename Straight Razor Vary: Accept-Language,Accept-Encoding,Origin X-Request-Id: 4e7b55af-236c-4fcd-ae09-18d88e9019c0 X-Runtime: 0.071640
  106. Finalizando...

  107. O Rails é uma excelente alternativa para APIs.

  108. Você tira todas as decisões que não são importantes da

    frente.
  109. Obrigado!