API D.O.C.G

with

Grape

API

Web app

Mobile App

...

...

Business Logic

External Services

Intridea

  • omniauth
  • hashie
  • oauth2
  • multi_json
  • ...

Grape gem

Grape lets you write a REST API easily as drink a glass of wine...

...by using the power of ruby on DSL:

  • Domain specific language
  • Human friendly
  • ...well, not so painfull!

ready to taste?

class WinesCollection::V1::Wines < Grape::API
  version 'v1', using: :path

  format :json
  
  content_type :json, 'application/json'
  
  formatter :json, JavascriptCaseFormatter

  helpers do
    def current_user
      User.find params[:user_id]
    end
  end

  resource :wines do
    desc 'search for wines.'
    params do
      optional :name, type: String
    end
    get :search do
      # ...
    end

    after do
      logger.info("#{current_user} loves wines!")
    end
  end
end

Routes

class WinesCollection::V1::Wines < Grape::API
  resource :wines do
    # ...
  end
  # alias_method: resources, group, resource, resources segment
end
GET/HEAD        get do # ...

POST            post do # ...

PUT             put do # ...
PATCH           patch do # ...

DELETE          delete do # ...

CUSTOM ROUTES!  route :any, '/path' do # ...

HTTP methods

Helpers

class WinesCollection::V1::Wines < Grape::API
  helpers do
    def current_user
      User.find(params.user_id)
    end

    def logger
      self.logger
    end
  end

  # ...

    get ':user_id' do
      logger.info("hello #{current_user.name}!")
    end
  
  # ...
end

Parameters

post '' do
  params         # => {"name"=>"Dolcetto", "bottling_date"=>"2015/03/31", "color"=>"red", "bac"=>"5.7", "acquisition_date"=>"2015/04/16", "image"=>"http://lol.com/dolcetto.jpg"}
  params.class   # => Hashie::Mash
  params['name'] # => "Dolcetto"
  params[:name]  # => "Dolcetto"
  params.name    # => "Dolcetto"
end
curl -X POST -H "Content-Type: application/json" -d '{
"name": "Dolcetto",
"bottling_date": "2015/03/31",
"color": "red",
"bac": "5.7",
"acquisition_date": "2015/04/16",
"image": "http://lol.com/dolcetto.jpg"
}' 'http://api.winescollection.com:3000/v1/wines'

...parse any kind of data...

  • URL encoded
  • JSON
  • XML
curl -X POST -H "Content-Type: application/json" -d '{"name": "Dolcetto",
"bottling_date": "2015/03/31","color": "red","bac": "5.7",
"acquisition_date": "2015/04/16","image": 
"http://lol.com/dolcetto.jpg"}' 'http://api.winescollection.com:3000/v1/wines'
curl -X POST -H "Content-Type: application/xml" -d 
'<wine>
    <name>Nebbiolo</name>
    <bottling_date>2015/05/31</bottling_date>
    <color>red</color>
    <bac>5.7</bac>
    <acquisition_date>2015/06/16</acquisition_date>
    <image>http://lol.com/nebbiolo.jpg</image>
</wine>' 'http://api.winescollection.com:3000/v1/wines'
curl -X POST -H "Content-Type: text/plain" 'http://api.winescollection.com:3000/v1/w
ines?name=Nebbiolo&bottling_date=red&color=5.7&acquisition_date=2015%2F06%2F16&image
=http%3A%2F%2Flol.com%2Fnebbiolo.jpg'

Decleared params

desc 'Create a new wine.'
params do
  requires :wine, type: Hash do
    requires :name, type: String
    requires :bottling_date, type: Date
    optional :image, type: String, regex: /(https?\:\/\/)?([a-zA-Z]+\.)+(org|com|it)/
    optional :bac, type: MyCustomType
    given :bac do
      requires :color, values: [:red, :white, :rose]
    end
  end
  requires :liquor, type: Hash do
    # ...
  end
  mutually_exclusive :liquor, :wine
  # exactly_one_of :wine
  # at_least_one_of :liquor
  # all_or_none_of :wine, :liquor
end
post do
  decleared_params = decleared(params) # params.slice(...)
end
  • Presence
  • Nested parameters
  • Types
    • built-in 
    • custom (self.parse)
  • Validations
    • regexp
    • values
    • default
    • custom (validate_param!)
  • Coercion
  • given & Co.
  • decleared!
  • ...

Custom Validators

class AlcoholLevel < Grape::Validations::Base
  def validate_param!(attr_name, params)
    if params[attr_name] <= @option
      fail Grape::Exceptions::Validation,
        params: [@scope.full_name(attr_name)],
        message: "This is not enough alcoholic!"
    end
  end
end



params do
  requires :bac, alcohol_level: 10.0
end

Callbacks

class WinesCollection::V1::Wines < Grape::API
  # ...

    before do
      # ...
    end

    before_validation do
      # ...
    end

    # parameter validations

    after_validation do
      # ...
    end

    get do # the actual API call
      # ...
    end

    after do
      # ...
    end
  
  # ...
end

API Version

Path

class WinesCollection::V1::Wines < Grape::API
  # ...

  version 'v1', using: :path

  # ...
end
curl 'http://api.winescollection.com:3000/v1/wines'

Headers

class WinesCollection::V1::Wines < Grape::API
  # ...

  version 'v1', using: :header, vendor: 'oc-agricola'

  # ...
end
curl -H Accept:application/vnd.oc-agricola-v1+json http://api.winescollection.com:3000/wines
class WinesCollection::V1::Wines < Grape::API
  # ...

  version 'v1', using: :accept_version_header

  # ...
end
curl -H Accept-Version:v1 http://api.winescollection.com:3000/wines

Parameter

class WinesCollection::V1::Wines < Grape::API
  # ...

  version 'v1', using: :param, parameter: 'ver'

  # ...
end
curl http://api.winescollection.com:3000/wines?ver=v1

Formats,

ContentType

and Formatters

class WinesCollection::V1::Wines < Grape::API
  # ...

  format :xml
  default_format :json

  content_type :xml, 'application/xml'
  content_type :json, 'application/json'
  
  formatter :json, ->(object, env) { object.to_js_case }
  formatter :json, JavascriptCaseFormatter # self.call

  # ...
end
  • define API formats
  • adds content_type details
  • post elaboration formatters

Presenting responses

class WinesCollection::V1::Wines < Grape::API
  # ...

    get ':user_id' do
      wine = Wine.find(params[:id])
   
      # ...

      present wine, with: WinesCollection::V1::Entities::Wine
    end
  
  # ...
end
module WinesCollection::V1::Entities
  class Wine < Grape:Entity
    expose :name

    expose :bac, as: :blood_alcohol_content

    expose :dates do
      expose :bottling_date, if: lambda { |instance, options| 
        # ...
      }
      expose :acquisition_date, safe: true
    end

    expose :image do |instance, options|
      downcase_file_format(instance.image)
    end

    expose :color, using: WineColor # < Grape::Entity

    def downcase_file_format(image)
      # ...
    end
  end
end
  • Method lookup
  • :as alias
  • Nested exposure
  • Conditional exposures.
  • safe
  • Runtime exposure.
  • Using other entities!
  • ...

Other presenters

  • rabl gem
  • grape-roar gem
  • ...

Composing API with the use of mount...

How to plant

Grape in a project

...grafting in Rails...

  1. Create app/api directory
  2. add stuff into config/application.rb:
    config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
    config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
  3. add stuff into config/routes.rb
    mount WinesCollection::API => '/'
  4. add other stuff in Gemfile:
    gem 'grape'
    gem 'hashie-forbidden_attributes'
    
  5. Enjoy Grape with all the power or Rails! 

...Rails is cool,

but it's too much,

I wanna less...

...grafting with Sinatra...

  1. Use the Rack stack.
  2. Create a config.ru file: 
    require 'sinatra'
    require 'grape'
    
    class WinesCollection::API < Grape::API
      # ...
    end
    
    class WinesCollectionApp < Sinatra::Base
      get '/' do
        # ...
      end
    end
    
    use Rack::Session::Cookie
    run Rack::Cascade.new [WinesCollection::API, WinesCollectionApp]
  3. Enjoy Grape with a lightweight framework!

...Seriously dude,

I do not need a framework,

I wanna less...

...grafting with ActiveRecord  (without Rails)...

  1. Use the Rack stack.
  2. Create a config.ru file: 
    use ActiveRecord::ConnectionAdapters::ConnectionManagement
    run WinesCollection::API
  3. Enjoy Grape with a touch of magic sql!

...well, I do not

even need a magic database adapter,

I still wanna less...

...grafting with

pure Rack!

  1. Use the Rack stack.
  2. Create a config.ru file: 
    run WinesCollection::API
  3. Enjoy Grape with... well, all that you want actually!

What about performance?

  • 2 processes / 2 threads
  • authentication
  • authorization OAuth2
  • Complex parameter parsing
  • Database hits
  • Full Text Search hit (facades)
  • Google Analytics event hit
  • Filesystem write (logs)
  • 1 session = 1 call
  • 135k calls/day
  • 200~300 calls/min

A world full of taste and colors!

Take a look at http://www.ruby-grape.org/projects/

  • Representers (entity, rabl, roar, ...)
  • Caching/Throttling
  • Pagination (will_paginate, kaminari)
  • Authentication/Authorization (doorkeeper, wine_bouncer)
  • Documentation (swagger)
  • logging/versioning (papertrail)
  • Monitoring (new_relic, librato, ...)
  • Testing (grape-entity-matchers)
  • Generators (scaffolding)
  • Microframeworks
  • ...

Recipe for

a good wine...

Do not break the Contract...

...wait,

which Contract?

"Always add never remove" strategy.

Advance version

Use HTTP

status codes

...and the most important...

...follow standards!

  • jsonapi.org
  • RFCs
  • look at top players tech blogs

Questions?

Wine Time!

API D.O.C.G with Grape

By Stefano Ordine

API D.O.C.G with Grape

  • 1,780