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

Building Better Web APIs with Rails

Building Better Web APIs with Rails

This talk covers things that developers need to know in order to provide a robust web API in Rails.

Carlos Souza

October 23, 2014
Tweet

More Decks by Carlos Souza

Other Decks in Technology

Transcript

  1. “We’ve detected the majority of our visitors come from mobile

    devices. 
 
 We need to launch a native app
 as soon as possible!”
  2. resources :projects GET
 POST
 PATCH (PUT) 
 DELETE ! !

    ... Projects#index Projects#create Projects#show Projects#update Projects#destroy ... paths methods actions /projects /projects/:id
  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. Client A API I’m a Rich Java$cript Application 
 and

    I want JSON! Hey, I’m an Enterprise Java Application and I want XML! (Ha Ha, Business!) ¯\_(ツ)_/¯ Oi, soy un browser e quiero HTML! response in JSON respuesta en HTML response in XML Client B Client C content negotiation The process in which client and server determine the best representation for a response
 when many are available.
  7. responders extracted out to 
 responders gem in Rails 4.2

    module API class ProjectsController < ApplicationController respond_to :json, :xml ! def index @projects = Project.all ! respond_with(@projects) end end end
  8. calls #to_json calls #to_xml respond_to 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
  9. 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
  10. class ProjectSerializer < ActiveModel::Serializer attributes :id, :title, :amount ! embed

    :ids, include: true has_many :products end defaults to JSON-API https://github.com/rails-api/active_model_serializers ActiveModel::Serializers
  11. 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 https://github.com/apotonick/roar using Mixins using Decorators Roar
  12. 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
  13. 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 http basic AUTH with curl
  14. • Can easily expire or regenerate tokens. • Any vulnerability

    is limited to API access. • Multiple tokens for each user. • Different access rules can be implemented. API Projects Forum Admin Client A token based auth
  15. token for the Digital Ocean API For security purposes, 


    some services will only display the access token once. providing the token
  16. token based auth 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
  17. end def generate_auth_token .gsub(/\-/,'') end 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 ... GENERATING TOKENS
  18. $ curl -IH "Authorization: Token token=16d7d6089b8fe0c5e19bfe10bb156832" \ http://localhost:3000/episodes ! HTTP/1.1

    200 OK Content-Type: application/json; charset=utf-8 use the -H option token based auth com curl
  19. MAJOR
 incompatible changes 
 MINOR
 backwards-compatible changes 
 PATCH
 backwards-compatible

    bug fixes http://semver.org/ Semantic versioning Works great for software libraries
  20. V1 /V1 feature X, feature Y /V2 feature X, feature

    Y, feature Z Compatible changes: • addition of a new format (i.e. JSON, XML ) • addition of a new property on a resource • renaming of an end-point (use 3xx status code!) • Only use major version. • Changes cannot break existing clients. • No need to bump version on compatible changes. versioning services
  21. 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”
  22. API HOW SHOULD WE TEST ? Requesting endpoints and verifying

    responses $ rails g integration_test <doing-something>
  23. testing status code 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
  24. testing status code 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
  25. testing 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
  26. testing 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
  27. 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 testing access rules
  28. 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 testing access rules