Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rock Solid Rails APIs

Rock Solid Rails APIs

Nesta palestra apresentada na RubyConf Brasil, vemos como o Ruby on Rails pode nos ajudar na construção de APIs robustas e de acordo com o protocolo HTTP. A palestra cobre configurações de rotas, Content Negotiation, Versionamento e Autenticação. Vemos também como testar nossas APIs, e algumas práticas que ajudam a manter nosso código organizado.

Carlos Souza

August 29, 2014
Tweet

More Decks by Carlos Souza

Other Decks in Technology

Transcript

  1. “Detectamos que a maioria dos visitantes acessa o site a

    partir de dispositivos móveis. Precisamos lançar um app nativo o mais rápido possível!”
  2. resources :projects REST Representational
 State Transfer Seguindo um permite a

    construção de uma infra-estrutura capaz de suportar diferentes tipos de aplicações conjunto estrito de operações $ rake routes Prefix Verb URI Pattern Controller#Action projects GET /projects(.:format) projects#index POST /projects(.:format) projects#create new_project GET /projects/new(.:format) projects#new edit_project GET /projects/:id/edit(.:format) projects#edit project GET /projects/:id(.:format) projects#show PATCH /projects/:id(.:format) projects#update PUT /projects/:id(.:format) projects#update DELETE /projects/:id(.:format) projects#destroy
  3. 
 ! get 'active' get 'suspended' ! ! ! post

    'activate' post 'suspend' ! ! resources :projects end end end do member do post 'archive' 
 collection do get 'archived' post 'create_review'
  4. 
 ! get 'active' get 'suspended' ! ! ! post

    'activate' post 'suspend' ! ! resources :projects end end end do member do post 'archive' 
 collection do get 'archived' , to: 'archived_projects#index' , to: 'active_projects#index' , to: 'suspended_projects#index' , to: 'archived_projects#create' , to: 'active_projects#create' , to: 'suspended_projects#create' post 'create_review'
  5. 
 ! get 'active' get 'suspended' ! ! ! post

    'activate' post 'suspend' ! ! resources :projects end end end do member do post 'archive' 
 collection do get 'archived' resources :reviews, only: :create , to: 'archived_projects#index' , to: 'active_projects#index' , to: 'suspended_projects#index' , to: 'archived_projects#create' , to: 'active_projects#create' , to: 'suspended_projects#create'
  6. resources :projects end do ... end http://foo.com/api/projects api/projects namespace :api

    do Prefix Verb URI Pattern Controller#Action api_projects GET /api/projects(.:format) api/projects#index POST /api/projects(.:format) api/projects#create new_api_project GET /api/projects/new(.:format) api/projects#new ...
  7. http://foo-dev.com:3000 http://api.foo-dev.com:3000 127.0.0.1 foo-dev.com 127.0.0.1 api.foo-dev.com /etc/hosts , constraints: {

    subdomain: 'api' }, path: '/' resources :projects end do ... end namespace :api do http://api.foo.com/projects projects api
  8. module API class ProjectsController < ApplicationController def index ... end

    end end ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'API' end config/initializers/inflections.rb
  9. module API class ProjectsController < ApplicationController ! ! def index

    @projects = Project.all end end end module API class SuspendedProjectsController < ApplicationController ! 
 def index @projects = Project.suspended end end end app/controllers/api/projects_controller.rb app/controllers/api/suspended_projects_controller.rb before_action :verify_access before_action :verify_access
  10. module API class BaseController < ApplicationController ! ! protected !

    def verify_access ... end end end app/controllers/api/base_controller.rb before_action :verify_access before_action :verify_access
  11. module API class ProjectsController < ! ! def index @projects

    = Project.all end end end module API class SuspendedProjectsController < ! 
 def index @projects = Project.suspended end end end BaseController BaseController
  12. Client A API I’m a mobile phone and I want

    JSON! Hey, I’m an Enterprise Java Application and I want XML! (Ha Ha, Business!) ¯\_(ツ)_/¯ Hola, yo soy un browser y quiero HTML! response in JSON respuesta in HTML response in XML Client B Client C O processo em que cliente e servidor determinam a melhor representação para a response. content negotiation
  13. module API class ProjectsController < ApplicationController respond_to :json, :xml !

    def index @projects = Project.all ! respond_with(@projects) end end end Extraída para uma gem a partir do Rails 4.2
  14. calls #to_json calls #to_xml module API class ProjectsController < ApplicationController

    def index @projects = Project.recent ! respond_to do |format| format.json { render json: @projects, status: 200 } format.xml { render xml: @projects, status: 200 } end end end end
  15. JBuilder json.content format_content(@message.content) json.(@message, :created_at, :updated_at) ! json.author do json.name

    @message.creator.name.familiar json.url url_for(@message.creator, format: :json) end https://github.com/rails/jbuilder
  16. class ProjectSerializer < ActiveModel::Serializer attributes :id, :title, :amount ! embed

    :ids, include: true has_many :products end defaults to JSON-API ActiveModel::Serializers https://github.com/rails-api/active_model_serializers
  17. module SongsRepresenter include Roar::JSON::JsonApi name :songs ! property :id property

    :title end class SongRepresenter < Roar::Decorator include Roar::JSON::JsonApi name :songs ! property :id property :title end Roar https://github.com/apotonick/roar usando Mixins usando Decorators
  18. http basic AUTH • Rápido e simples • Reutiliza credenciais

    existentes • Parte da spec. HTTP (RFC 2617)
  19. http basic AUTH module API class ProjectsController < ApplicationController before_action

    :authenticate_or_request ! protected ! def authenticate_or_request authenticate_or_request_with_http_basic do |user, pwd| User.authenticate(user, pwd) end end end end
  20. http basic AUTH com curl use the -u option $

    curl -I http://carlos:secret@localhost:3000/projects ! HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 $ curl -Iu 'carlos:secret' http://localhost:3000/projects ! HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8
  21. • Can easily expire or regenerate tokens. • Any vulnerability

    is limited to API access. • Multiple tokens for each user. • Different access rules can be implemented. token based auth API Projects Forum Admin Client A
  22. token para a API do Backpack adquirindo o token Normalmente

    disponível “out of band”,
 na página de profile ou settings.
  23. adquirindo o token Em alguns serviços, o token é exibido

    apenas uma vez por motivo de segurança. token para a API da Digital Ocean
  24. module API class ProjectsController < ApplicationController before_action :authenticate_or_request ! protected

    ! def authenticate_or_request authenticate_or_request_with_http_token do |token, opt| User.find_by(auth_token: token) end end end end token based auth
  25. A Ruby stdlib oferece um método para geração de UUIDs

    GErando TOKENS SecureRandom.uuid f4ea855f-d303-43e6-bee3-94581c0ecb21 90ab3255-ce33-4022-8349-b7979655b07c 371c760d-2539-41b9-b665-98c255d4c323 ...
  26. end def generate_auth_token .gsub(/\-/,'') end GErando TOKENS omits the hyphens

    SecureRandom.uuid class User < ActiveRecord::Base before_create :set_auth_token ! private ! def set_auth_token return if auth_token.present?
 self.auth_token = generate_auth_token end ! a47a8e54b11c4de5a4a351734c80a14a 9fa8a147b10c4efca3e8592b3a1c2729 823c1c984d504f66a2e6cbb2eb69e842 ...
  27. $ curl -IH "Authorization: Token token=16d7d6089b8fe0c5e19bfe10bb156832" \ http://localhost:3000/episodes ! HTTP/1.1

    200 OK Content-Type: application/json; charset=utf-8 HTTP Request Header token based auth com curl
  28. Patch Minor Major versionamento semântico MAJOR
 mudanças incompatíveis 
 MINOR


    novas features backwards-compatible 
 PATCH
 bug fixes http://semver.org/
  29. V1 /V1 feature X, feature Y /V2 feature X, feature

    Y, feature Z VERSIONANDO SERVIÇOS Mudanças não podem quebrar clientes existentes. Exemplo de mudanças compatíveis: • suporte a um novo formato (i.e. JSON, XML) • nova propriedade em um resource Apenas major version. (não necessitam version bump)
  30. https://www.mnot.net/blog/2012/12/04/api-evolution “The biggest way to avoid ! new major versions

    is to make as many of your changes backwards-compatible as possible”
  31. API testando APIs Não é hora de testar regras de

    negócio (core business) • Status Code • Mime Types • Regras de Acesso Testar Unit Tests
  32. API verificando status codes require 'test_helper' ! class ListingProjectsTest <

    ActionDispatch::IntegrationTest setup { host! 'api.example.com' } ! test 'returns list of projects' do get '/projects' assert_equal 200, response.status refute_empty response.body end end
  33. API verificando mime types class ListingProjectsTest < ActionDispatch::IntegrationTest setup {

    host! 'api.example.com' } 
 test 'returns projects in JSON' do get '/projects', {}, { 'Accept' => Mime::JSON } assert_equal Mime::JSON, response.content_type end test 'returns projects in XML' do get '/projects', {}, { 'Accept' => Mime::XML } assert_equal Mime::XML, response.content_type end end
  34. verificando regras de acesso class ListingProjectsTest < ActionDispatch::IntegrationTest setup {

    @user = User.create! } setup { host! 'api.example.com' } ! test 'valid authentication with token' do get '/projects', {}, { 'Authorization' => "Token token=#{@user.auth_token}"} assert_equal 200, response.status assert_equal Mime::JSON, response.content_type end ! test 'invalid authentication' do get '/projects', {}, { 'Authorization' => "Token token=#{@user.auth_token}fake” } assert_equal 401, response.status end end