About me
Andriy Savchenko /ptico
CTO Aejis
andriy@aejis.eu
RubyMeditation
W A R N I N G
THIS TALK
CONTAINS
LOTS

OF CODE
THIS IS YOUR LAST CHANCE TO LEAVE AUDITORY
Problems with rails
• Low latency
• Dependency hell
• MVC is only suitable for simple CRUD
• ActiveSupport
Other frameworks
• grape
• sinatra
• rum
• nyny
And…
Rack
run ->(env) {
[
200, # <= Response code
{'Content-Type' => 'application/json'}, # <= Headers
[ '{"a": 1}' ] # <= Body
]
}
require 'json'
run ->(env) {
[
200,
{'Content-Type' => 'application/json'},
[ JSON.dump({ a: 1 }) ] # <= Almost API ;)
]
}
Add some OOP
run ->(env) {
[
200, # <= Response code
{'Content-Type' => 'application/json'}, # <= Headers
[ '{"a": 1}' ] # <= Body
]
}
class Responder
def response_code
200
end
def headers
{'Content-Type' => 'application/json'}
end
def body
[ JSON.dump({ a: 1 }) ]
end
end
class Responder
def response_code
@code
end
def headers
@headers
end
def body
[ JSON.dump(@body) ]
end
end
class Example < Responder
def initialize(env)
@code = 200
@headers = {'Content-Type' => 'application/json'}
@body = { a: 1 }
end
end
class ReadUsers < Responder
def initialize(env)
@code = 200
@headers = {'Content-Type' => 'application/json'}
@body = DB[:users].all
end
end
run ->(env) {
result = ReadUsers.new(env)
[result.response_code, result.headers, result.body]
}
result = ReadUsers.new(env)
r = Nginx::Request.new
result.headers.each_pair { |k, v| r.headers_out[k] = v }
Nginx.rputs result.body[0]
Nginx.return result.response_code
Less generic example
Good API
• Proper status codes
• Compatibility (?suppress_response_code=true)
• Metadata
class Responder
class << self
def call(env)
req = ::Rack::Request.new(env)
instance = new(req)
instance.call
instance.to_rack_array
end
end
attr_reader :request, :params, :headers
def initialize(req)
@request = req
@params = req.params
@headers = default_response_headers
end
def call; end
def to_rack_array
[http_response_code, http_response_headers, http_response_body]
end
end
class Responder
def response_code
@response_code || default_response_code
end
private
def default_response_code
200
end
def http_response_code
params['suppress_response_codes'] ? 200 : response_code
end
end
class Responder
def default_response_headers
{ 'Content-Type' => 'application/json' }.dup
end
def http_response_headers
@headers
end
end
class Responder
def body
@body
end
private
def http_response_body
[ JSON.dump(body) ]
end
end
class ReadUsers < Responder
def call
@body = DB[:users].all
end
end
class Read < Responder
def call
@body = fetch
end
end
class ReadUsers < Read
def fetch
DB[:users].all
end
end
class Write < Responder
def call
@body = valid_params? ? success : failure
end
private
def success; end
def failure; end
def valid_params?
true
end
end
class CreateUser < Write
def default_response_code
201
end
def valid_params?
params['login'] && params['email']
end
def success
DB[:users].insert(params)
end
def failure
@response_code = 400
{ error: 'Invalid params' }
end
end
class Responder
def body
{
code: http_response_code,
result: @body,
meta: meta
}
end
def meta
{
server_time: Time.now.to_i
}
end
end
{
"code": 200,
"result": [
{
"id": 1,
"name": "Andriy Savchenko",
"email": "andriy@aejis.eu",
"company": "Aejis",
"hiring": true
}
],
"meta": {
"server_time": 1447939835
}
}
Awesome!
Routers
• Rack::Builder
• http_router (gh:joshbuddy/http_router)
• lotus-router (gh:lotus/router)
• signpost (gh:Ptico/signpost)
• journey (dead)
Advantages
Faster
$ ruby -v
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin15]
$ puma -e production
$ ab -n 10000 -c 100 http://0.0.0.0:9292/users/
|======================|====Rails-API====|=====Sinatra=====|=====Rack API====|
|Time taken for tests: | 13.262 seconds | 6.858 seconds | 3.665 seconds |
|Complete requests: | 10000 | 10000 | 10000 |
|Failed requests: | 0 | 0 | 0 |
|Requests per second: | 754.03 [#/sec] | 1458.20 [#/sec] | 2728.28 [#/sec] |
|Time per request: | 132.620 [ms] | 68.578 [ms] | 36.653 [ms] |
|Time per request (c): | 1.326 [ms] | 0.686 [ms] | 0.367 [ms] |
|Transfer rate: | 301.91 [KB/sec] | 262.02 [KB/sec] | 402.31 [KB/sec] |
|============================================================================|
Faster
• 4x faster then rails-api & 2x then sinatra
• Ready for further improvements
Magic-less
• Base responder takes ≈ 65LOC
• The only dependency is Rack (optional)
Maintainable
• Stable object interface
• Each responder can have its own file structure
• SOLID
• Test-friendly
Questions?
Credits and attributions:
• Title illustration by Max Bohdanowski
• Lobster Two font by Pablo Impallari & Igino Marini (OFL)
• Font Awesome by Dave Gandy - http://fontawesome.io (OFL)
• https://www.flickr.com/photos/mattsh/14194586111/ (CC BY-NC-SA 2.0)
Andriy Savchenko /ptico
andriy@aejis.eu

Building Web-API without Rails, Registration or SMS

  • 2.
    About me Andriy Savchenko/ptico CTO Aejis andriy@aejis.eu
  • 3.
  • 4.
    W A RN I N G THIS TALK CONTAINS LOTS
 OF CODE THIS IS YOUR LAST CHANCE TO LEAVE AUDITORY
  • 5.
  • 6.
    • Low latency •Dependency hell • MVC is only suitable for simple CRUD • ActiveSupport
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
    run ->(env) { [ 200,# <= Response code {'Content-Type' => 'application/json'}, # <= Headers [ '{"a": 1}' ] # <= Body ] }
  • 12.
    require 'json' run ->(env){ [ 200, {'Content-Type' => 'application/json'}, [ JSON.dump({ a: 1 }) ] # <= Almost API ;) ] }
  • 13.
  • 14.
    run ->(env) { [ 200,# <= Response code {'Content-Type' => 'application/json'}, # <= Headers [ '{"a": 1}' ] # <= Body ] }
  • 15.
    class Responder def response_code 200 end defheaders {'Content-Type' => 'application/json'} end def body [ JSON.dump({ a: 1 }) ] end end
  • 16.
    class Responder def response_code @code end defheaders @headers end def body [ JSON.dump(@body) ] end end
  • 17.
    class Example <Responder def initialize(env) @code = 200 @headers = {'Content-Type' => 'application/json'} @body = { a: 1 } end end
  • 18.
    class ReadUsers <Responder def initialize(env) @code = 200 @headers = {'Content-Type' => 'application/json'} @body = DB[:users].all end end
  • 19.
    run ->(env) { result= ReadUsers.new(env) [result.response_code, result.headers, result.body] }
  • 20.
    result = ReadUsers.new(env) r= Nginx::Request.new result.headers.each_pair { |k, v| r.headers_out[k] = v } Nginx.rputs result.body[0] Nginx.return result.response_code
  • 21.
  • 22.
    Good API • Properstatus codes • Compatibility (?suppress_response_code=true) • Metadata
  • 23.
    class Responder class <<self def call(env) req = ::Rack::Request.new(env) instance = new(req) instance.call instance.to_rack_array end end attr_reader :request, :params, :headers def initialize(req) @request = req @params = req.params @headers = default_response_headers end def call; end def to_rack_array [http_response_code, http_response_headers, http_response_body] end end
  • 24.
    class Responder def response_code @response_code|| default_response_code end private def default_response_code 200 end def http_response_code params['suppress_response_codes'] ? 200 : response_code end end
  • 25.
    class Responder def default_response_headers {'Content-Type' => 'application/json' }.dup end def http_response_headers @headers end end
  • 26.
    class Responder def body @body end private defhttp_response_body [ JSON.dump(body) ] end end
  • 27.
    class ReadUsers <Responder def call @body = DB[:users].all end end
  • 28.
    class Read <Responder def call @body = fetch end end
  • 29.
    class ReadUsers <Read def fetch DB[:users].all end end
  • 30.
    class Write <Responder def call @body = valid_params? ? success : failure end private def success; end def failure; end def valid_params? true end end
  • 31.
    class CreateUser <Write def default_response_code 201 end def valid_params? params['login'] && params['email'] end def success DB[:users].insert(params) end def failure @response_code = 400 { error: 'Invalid params' } end end
  • 32.
    class Responder def body { code:http_response_code, result: @body, meta: meta } end def meta { server_time: Time.now.to_i } end end
  • 33.
    { "code": 200, "result": [ { "id":1, "name": "Andriy Savchenko", "email": "andriy@aejis.eu", "company": "Aejis", "hiring": true } ], "meta": { "server_time": 1447939835 } }
  • 34.
  • 35.
    Routers • Rack::Builder • http_router(gh:joshbuddy/http_router) • lotus-router (gh:lotus/router) • signpost (gh:Ptico/signpost) • journey (dead)
  • 36.
  • 37.
    Faster $ ruby -v ruby2.2.3p173 (2015-08-18 revision 51636) [x86_64-darwin15] $ puma -e production $ ab -n 10000 -c 100 http://0.0.0.0:9292/users/ |======================|====Rails-API====|=====Sinatra=====|=====Rack API====| |Time taken for tests: | 13.262 seconds | 6.858 seconds | 3.665 seconds | |Complete requests: | 10000 | 10000 | 10000 | |Failed requests: | 0 | 0 | 0 | |Requests per second: | 754.03 [#/sec] | 1458.20 [#/sec] | 2728.28 [#/sec] | |Time per request: | 132.620 [ms] | 68.578 [ms] | 36.653 [ms] | |Time per request (c): | 1.326 [ms] | 0.686 [ms] | 0.367 [ms] | |Transfer rate: | 301.91 [KB/sec] | 262.02 [KB/sec] | 402.31 [KB/sec] | |============================================================================|
  • 38.
    Faster • 4x fasterthen rails-api & 2x then sinatra • Ready for further improvements
  • 39.
    Magic-less • Base respondertakes ≈ 65LOC • The only dependency is Rack (optional)
  • 40.
    Maintainable • Stable objectinterface • Each responder can have its own file structure • SOLID • Test-friendly
  • 41.
    Questions? Credits and attributions: •Title illustration by Max Bohdanowski • Lobster Two font by Pablo Impallari & Igino Marini (OFL) • Font Awesome by Dave Gandy - http://fontawesome.io (OFL) • https://www.flickr.com/photos/mattsh/14194586111/ (CC BY-NC-SA 2.0) Andriy Savchenko /ptico andriy@aejis.eu