$30 off During Our Annual Pro Sale. View Details »

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.

Nando Vieira

May 18, 2013
Tweet

More Decks by Nando Vieira

Other Decks in Programming

Transcript

  1. RAILS PARA APIS
    NANDO VIEIRA @fnando

    View Slide

  2. http://nandovieira.com.br

    View Slide

  3. http://hellobits.com

    View Slide

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

    View Slide

  5. http://codeplane.com.br

    View Slide

  6. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  11. APIs tem que ser rápidas.

    View Slide

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

    View Slide

  13. No Sinatra, você volta ao
    problema de ter que fazer tudo o
    que o Rails já fazia por você.

    View Slide

  14. Rails para API = Rails - Web app stuff

    View Slide

  15. Instalando

    View Slide

  16. $ gem install rails --pre

    View Slide

  17. $ rails new codeplane -TJ -d postgresql

    View Slide

  18. Dependências

    View Slide

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

    View Slide

  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"

    View Slide

  21. Middlewares

    View Slide

  22. O Rails possui uma série de
    middlewares que não são
    necessários para APIs.

    View Slide

  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

    View Slide

  24. Recebendo parâmetros

    View Slide

  25. Desabilite o CSRF token.

    View Slide

  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

    View Slide

  27. No Rails, estamos acostumados a
    receber hashes como POST data.

    View Slide

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

    View Slide

  29. {
    name: "John Doe",
    email: "[email protected]",
    password: "[FILTERED]",
    password_confirmation: "[FILTERED]"
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  34. params[:name]
    #=> hello

    View Slide

  35. request.request_parameters

    View Slide

  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

    View Slide

  37. # app/controllers/application_controller.rb
    class ApplicationController < ActionController::Base
    def body_data
    @body_data ||= BodyData.new(request.request_parameters)
    end
    end

    View Slide

  38. repo = Repo.new body_data.only(:name)

    View Slide

  39. Representando dados

    View Slide

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

    View Slide

  41. JSON
    JavaScript Object Notation

    View Slide

  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

    View Slide

  43. Crie uma camada de abstração
    dos dados que serão
    representados.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  47. http localhost:3002/repos

    View Slide

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

    View Slide

  49. Schemaless é bom, mas é ruim.

    View Slide

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

    View Slide

  51. http://jsonapi.org

    View Slide

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

    View Slide

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

    View Slide

  54. # app/controllers/repos_controller.rb
    class ReposController < ApplicationController
    include Roar::Rails::ControllerAdditions
    respond_to :json
    def index
    respond_with Repo.all
    end
    end

    View Slide

  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

    View Slide

  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

    View Slide

  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"
    }
    ]
    }

    View Slide

  58. O Rails possui suporte às
    convenções REST desde o Rails 2.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  62. http http://localhost:3000/

    View Slide

  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"
    }
    }
    }

    View Slide

  64. http http://localhost:3000/help/locales

    View Slide

  65. {
    "_links": {
    "self": {
    "href": "http://localhost:3000/help/locales"
    }
    },
    "locales": [
    {
    "code": "en",
    "name": "English"
    },
    {
    "code": "pt-BR",
    "name": "Brazilian Portuguese"
    }
    ]
    }

    View Slide

  66. http OPTIONS http://localhost:3000/users

    View Slide

  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

    View Slide

  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

    View Slide

  69. # config/initializers/action_dispatch.rb
    class ActionDispatch::Routing::Mapper
    module HttpHelpers
    def allow(*verbs)
    AllowedMethods.new(*verbs)
    end
    end
    end

    View Slide

  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

    View Slide

  71. http POST http://localhost:3000/users

    View Slide

  72. {
    "errors": {
    "email": [
    "is invalid"
    ],
    "name": [
    "can't be blank"
    ],
    "password": [
    "can't be blank"
    ],
    "username": [
    "is invalid"
    ]
    }
    }

    View Slide

  73. http POST http://localhost:3000/users \
    Accept-Language:pt-BR

    View Slide

  74. {
    "errors": {
    "email": [
    "é inválido"
    ],
    "name": [
    "não pode ficar em branco"
    ],
    "password": [
    "não pode ficar em branco"
    ],
    "username": [
    "não é válido"
    ]
    }
    }

    View Slide

  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

    View Slide

  76. Autenticação

    View Slide

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

    View Slide

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

    View Slide

  79. Comece simples e mude a
    autenticação no futuro se for
    necessário.

    View Slide

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

    View Slide

  81. authenticate_or_request_with_http_token(realm, &block)

    View Slide

  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

    View Slide

  83. http localhost:3002/repos Authorization:"Token token=abc"

    View Slide

  84. Exija HTTPS.

    View Slide

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

    View Slide

  86. http://www.cheapssls.com

    View Slide

  87. Veracidade

    View Slide

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

    View Slide

  89. Assinatura

    View Slide

  90. secret + timestamp + url + data

    View Slide

  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")

    View Slide

  92. components = [url, secret, timestamp]
    components += options
    .keys
    .sort
    .reduce(components) do |buffer, key|
    buffer.concat [key, options[key]]
    end

    View Slide

  93. signature = OpenSSL::HMAC.hexdigest(
    digest,
    secret,
    components.join("")
    )

    View Slide

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

    View Slide

  95. Caching

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  99. http http://localhost:3000/

    View Slide

  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

    View Slide

  101. GZIP

    View Slide

  102. Sirva compressão GZIP para
    quem entende.

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  106. Finalizando...

    View Slide

  107. O Rails é uma excelente
    alternativa para APIs.

    View Slide

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

    View Slide

  109. Obrigado!

    View Slide