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...
- Create app/api directory
- 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', '*')]
- add stuff into config/routes.rb
mount WinesCollection::API => '/'
- add other stuff in Gemfile:
gem 'grape' gem 'hashie-forbidden_attributes'
- Enjoy Grape with all the power or Rails!
...Rails is cool,
but it's too much,
I wanna less...
...grafting with Sinatra...
- Use the Rack stack.
- 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]
- Enjoy Grape with a lightweight framework!
...Seriously dude,
I do not need a framework,
I wanna less...
...grafting with ActiveRecord (without Rails)...
- Use the Rack stack.
- Create a config.ru file:
use ActiveRecord::ConnectionAdapters::ConnectionManagement run WinesCollection::API
- 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!
- Use the Rack stack.
- Create a config.ru file:
run WinesCollection::API
- 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,782