$30 off During Our Annual Pro Sale. View Details »

Building web-API without Rails, registration or sms

Building web-API without Rails, registration or sms

Pivorak meetup #6, Lviv

Andrey Savchenko

November 20, 2015
Tweet

More Decks by Andrey Savchenko

Other Decks in Programming

Transcript

  1. View Slide

  2. About me
    Andriy Savchenko /ptico
    CTO Aejis
    [email protected]

    View Slide

  3. RubyMeditation

    View Slide

  4. W A R N I N G
    THIS TALK
    CONTAINS
    LOTS

    OF CODE
    THIS IS YOUR LAST CHANCE TO LEAVE AUDITORY

    View Slide

  5. Problems with rails

    View Slide

  6. • Low latency
    • Dependency hell
    • MVC is only suitable for simple CRUD
    • ActiveSupport

    View Slide

  7. Other frameworks

    View Slide

  8. • grape
    • sinatra
    • rum
    • nyny

    View Slide

  9. And…

    View Slide

  10. Rack

    View Slide

  11. run ->(env) {
    [
    200, # <= Response code
    {'Content-Type' => 'application/json'}, # <= Headers
    [ '{"a": 1}' ] # <= Body
    ]
    }

    View Slide

  12. require 'json'
    run ->(env) {
    [
    200,
    {'Content-Type' => 'application/json'},
    [ JSON.dump({ a: 1 }) ] # <= Almost API ;)
    ]
    }

    View Slide

  13. Add some OOP

    View Slide

  14. run ->(env) {
    [
    200, # <= Response code
    {'Content-Type' => 'application/json'}, # <= Headers
    [ '{"a": 1}' ] # <= Body
    ]
    }

    View Slide

  15. class Responder
    def response_code
    200
    end
    def headers
    {'Content-Type' => 'application/json'}
    end
    def body
    [ JSON.dump({ a: 1 }) ]
    end
    end

    View Slide

  16. class Responder
    def response_code
    @code
    end
    def headers
    @headers
    end
    def body
    [ JSON.dump(@body) ]
    end
    end

    View Slide

  17. class Example < Responder
    def initialize(env)
    @code = 200
    @headers = {'Content-Type' => 'application/json'}
    @body = { a: 1 }
    end
    end

    View Slide

  18. class ReadUsers < Responder
    def initialize(env)
    @code = 200
    @headers = {'Content-Type' => 'application/json'}
    @body = DB[:users].all
    end
    end

    View Slide

  19. run ->(env) {
    result = ReadUsers.new(env)
    [result.response_code, result.headers, result.body]
    }

    View Slide

  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

    View Slide

  21. Less generic example

    View Slide

  22. Good API
    • Proper status codes
    • Compatibility (?suppress_response_code=true)
    • Metadata

    View Slide

  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

    View Slide

  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

    View Slide

  25. class Responder
    def default_response_headers
    { 'Content-Type' => 'application/json' }.dup
    end
    def http_response_headers
    @headers
    end
    end

    View Slide

  26. class Responder
    def body
    @body
    end
    private
    def http_response_body
    [ JSON.dump(body) ]
    end
    end

    View Slide

  27. class ReadUsers < Responder
    def call
    @body = DB[:users].all
    end
    end

    View Slide

  28. class Read < Responder
    def call
    @body = fetch
    end
    end

    View Slide

  29. class ReadUsers < Read
    def fetch
    DB[:users].all
    end
    end

    View Slide

  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

    View Slide

  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

    View Slide

  32. class Responder
    def body
    {
    code: http_response_code,
    result: @body,
    meta: meta
    }
    end
    def meta
    {
    server_time: Time.now.to_i
    }
    end
    end

    View Slide

  33. {
    "code": 200,
    "result": [
    {
    "id": 1,
    "name": "Andriy Savchenko",
    "email": "[email protected]",
    "company": "Aejis",
    "hiring": true
    }
    ],
    "meta": {
    "server_time": 1447939835
    }
    }

    View Slide

  34. Awesome!

    View Slide

  35. Routers
    • Rack::Builder
    • http_router (gh:joshbuddy/http_router)
    • lotus-router (gh:lotus/router)
    • signpost (gh:Ptico/signpost)
    • journey (dead)

    View Slide

  36. Advantages

    View Slide

  37. 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] |
    |============================================================================|

    View Slide

  38. Faster
    • 4x faster then rails-api & 2x then sinatra
    • Ready for further improvements

    View Slide

  39. Magic-less
    • Base responder takes ≈ 65LOC
    • The only dependency is Rack (optional)

    View Slide

  40. Maintainable
    • Stable object interface
    • Each responder can have its own file structure
    • SOLID
    • Test-friendly

    View Slide

  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
    [email protected]

    View Slide