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

Building Ambitious APIs with Ruby

Building Ambitious APIs with Ruby

Presented at the Burlington Ruby Conference 2013. Covers active_model_serializers, the JSON API project, hypermedia APIs and general API design.

Dan Gebhardt

August 04, 2013
Tweet

More Decks by Dan Gebhardt

Other Decks in Programming

Transcript

  1. BUILDING AMBITIOUS APIS
    WITH RUBY
    Dan Gebhardt
    @dgeb
    Burlington Ruby 2013

    View Slide

  2. DO YOU NEED AN API?

    View Slide

  3. CLIENT-SIDE JS APPLICATION?

    View Slide

  4. CLIENT-SIDE JS APPLICATION?
    YOU NEED AN API

    View Slide

  5. NATIVE APPLICATION?

    View Slide

  6. NATIVE APPLICATION?
    YOU NEED AN API

    View Slide

  7. 3RD PARTY INTEGRATION?
    Creative Commons image courtesy of
    Kamil Porembiński

    View Slide

  8. 3RD PARTY INTEGRATION?
    YOU NEED AN API
    Image credit: http://up1ter.deviantart.com/art/Twilight-Sparkle-is-flying-286029311

    View Slide

  9. SCRIPTABLE?

    View Slide

  10. SCRIPTABLE?
    YOU NEED AN API
    @devops_borat @hipsterhacker
    @neckbeardhacker

    View Slide

  11. SO DO YOU NEED AN API?

    View Slide

  12. SO DO YOU NEED AN API?
    *PROBABLY*

    View Slide

  13. LET'S BUILD SOMETHING
    THAT DOES

    View Slide

  14. RubyPets!

    View Slide

  15. RubyPets!

    View Slide

  16. EMBER UI

    View Slide

  17. ANDROID & IOS APPS
    EMBER UI

    View Slide

  18. ANDROID & IOS APPS
    EMBER UI
    LOLCATS INTEGRATION

    View Slide

  19. ANDROID & IOS APPS
    LOLCATS INTEGRATION
    EMBER UI
    NEEDS AN API

    View Slide

  20. "WE NEED A
    RESTFUL API"

    View Slide

  21. "...AFTER YOU FEED ME!"

    View Slide

  22. RESTFUL API BASICS

    View Slide

  23. Your application's
    NOUNS
    are modeled as
    RESOURCES

    View Slide

  24. OUR RESOURCES
    Pets

    View Slide

  25. OUR RESOURCES
    People

    View Slide

  26. Every
    RESOURCE
    has a URI

    View Slide

  27. OUR RESOURCES
    # all pets
    /pets
    # each pet
    /pets/123

    View Slide

  28. OUR RESOURCES
    # all people
    /people
    # each person
    /people/123

    View Slide

  29. Resources are
    acted upon with
    HTTP METHODS

    View Slide

  30. POST
    GET
    UPDATE
    PATCH
    DELETE
    /pets/123
    /pets/
    /pets/123
    /pets/123
    /pets/123

    View Slide

  31. POST
    GET
    UPDATE
    PATCH
    DELETE
    /pets/123
    /pets/
    /pets/123
    /pets/123
    /pets/123

    View Slide

  32. POST
    GET
    UPDATE
    PATCH
    DELETE
    /pets/123
    /pets/
    /pets/123
    /pets/123
    /pets/123

    View Slide

  33. POST
    GET
    UPDATE
    PATCH
    DELETE
    /pets/123
    /pets/
    /pets/123
    /pets/123
    /pets/123

    View Slide

  34. POST
    GET
    UPDATE
    PATCH
    DELETE
    /pets/123
    /pets/
    /pets/123
    /pets/123
    /pets/123

    View Slide

  35. POST
    GET
    UPDATE
    PATCH
    DELETE
    /pets/123
    /pets/
    /pets/123
    /pets/123
    /pets/123
    + MORE

    View Slide

  36. "WE NEED AN
    AMBITIOUS
    RESTFUL API!"

    View Slide

  37. DEFINE AMBITIOUS

    View Slide

  38. DEFINE AMBITIOUS
    •Flexible

    View Slide

  39. DEFINE AMBITIOUS
    •Flexible
    •Consistent

    View Slide

  40. DEFINE AMBITIOUS
    •Flexible
    •Consistent
    •Efficient

    View Slide

  41. DEFINE AMBITIOUS
    •Flexible
    •Consistent
    •Efficient
    •Secure

    View Slide

  42. DEFINE AMBITIOUS
    •Flexible
    •Consistent
    •Efficient
    •Secure
    •Discoverable

    View Slide

  43. IS THERE ANYTHING
    WE WON'T DO?

    View Slide

  44. LET'S STICK WITH JSON
    ... AND SKIP XML

    View Slide

  45. OK, LET'S GET STARTED!

    View Slide

  46. OK, LET'S GET STARTED!

    View Slide

  47. LET'S START SIMPLE
    $ rails new rubypets
    $ cd rubypets
    $ rails g scaffold pet name:string
    $ rails g scaffold toy name:string pet:references
    $ rake db:migrate
    # set a default route, define relationships, etc ...

    View Slide

  48. LET'S START SIMPLE
    $ rails new rubypets
    $ cd rubypets
    $ rails g scaffold pet name:string
    $ rails g scaffold toy name:string pet:references
    $ rake db:migrate
    # set a default route, define relationships, etc ...

    View Slide

  49. LET'S START SIMPLE
    $ rails new rubypets
    $ cd rubypets
    $ rails g scaffold pet name:string
    $ rails g scaffold toy name:string pet:references
    $ rake db:migrate
    # set a default route, define relationships, etc ...

    View Slide

  50. LET'S START SIMPLE
    $ rails new rubypets
    $ cd rubypets
    $ rails g scaffold pet name:string
    $ rails g scaffold toy name:string pet:references
    $ rake db:migrate
    # set a default route, define relationships, etc ...

    View Slide

  51. LET'S START SIMPLE
    $ rails new rubypets
    $ cd rubypets
    $ rails g scaffold pet name:string
    $ rails g scaffold toy name:string pet:references
    $ rake db:migrate
    # set a default route, define relationships, etc ...

    View Slide

  52. LET'S START SIMPLE
    $ rails new rubypets
    $ cd rubypets
    $ rails g scaffold pet name:string
    $ rails g scaffold toy name:string pet:references
    $ rake db:migrate
    # set a default route, define relationships, etc ...

    View Slide

  53. MODELS
    # /app/models/pet.rb
    class Pet < ActiveRecord::Base
    has_many :toys
    end
    # /app/models/toy.rb
    class Toy < ActiveRecord::Base
    belongs_to :pet
    end

    View Slide

  54. CONTROLLERS
    class PetsController < ApplicationController
    before_action :set_pet, only: [:show, :edit, :update, :destroy]
    # GET /pets
    # GET /pets.json
    def index
    @pets = Pet.all
    end
    # GET /pets/1
    # GET /pets/1.json
    def show
    end
    .....
    end

    View Slide

  55. JBUILDER VIEWS
    # pets/index.json.jbuilder
    json.array!(@pets) do |pet|
    json.extract! pet, :name
    json.url pet_url(pet, format: :json)
    end
    # pets/show.json.jbuilder
    json.extract! @pet, :name, :created_at, :updated_at

    View Slide

  56. OUTPUT
    # /pets.json
    [
    {"name":"Cashew",
    "url":"http://localhost:3000/pets/1.json"},
    {"name":"Ginkgo",
    "url":"http://localhost:3000/pets/2.json"}
    ]
    # /pets/2.json
    {"name":"Ginkgo",
    "created_at":"2013-08-02T04:03:06.958Z",
    "updated_at":"2013-08-02T04:03:06.958Z"}
    JSON

    View Slide

  57. OUTPUT
    # /pets.json
    [
    {"name":"Cashew",
    "url":"http://localhost:3000/pets/1.json"},
    {"name":"Ginkgo",
    "url":"http://localhost:3000/pets/2.json"}
    ]
    # /pets/2.json
    {"name":"Ginkgo",
    "created_at":"2013-08-02T04:03:06.958Z",
    "updated_at":"2013-08-02T04:03:06.958Z"}
    JSON

    View Slide

  58. OUTPUT
    # /pets.json
    [
    {"name":"Cashew",
    "url":"http://localhost:3000/pets/1.json"},
    {"name":"Ginkgo",
    "url":"http://localhost:3000/pets/2.json"}
    ]
    # /pets/2.json
    {"name":"Ginkgo",
    "created_at":"2013-08-02T04:03:06.958Z",
    "updated_at":"2013-08-02T04:03:06.958Z"}
    "WE NEED CONSISTENCY !!!"
    JSON

    View Slide

  59. JBUILDER VIEWS
    # pets/index.json.jbuilder
    json.array!(@pets) do |pet|
    json.extract! pet, :name
    json.url pet_url(pet, format: :json)
    end
    # pets/show.json.jbuilder
    json.extract! @pet, :name, :created_at, :updated_at

    View Slide

  60. "CUSTOM" ISN'T ALWAYS A
    GOOD THING

    View Slide

  61. WE NEED MORE
    CONVENTION
    AND LESS
    CONFIGURATION

    View Slide

  62. View Slide

  63. View Slide

  64. View Slide

  65. View Slide

  66. View Slide

  67. View Slide

  68. View Slide

  69. View Slide

  70. View Slide

  71. View Slide

  72. View Slide

  73. LET'S TRY IT OUT
    # Gemfile
    # gem 'jbuilder', '~> 1.2'
    gem 'active_model_serializers'

    View Slide

  74. LET'S TRY IT OUT
    # Gemfile
    # gem 'jbuilder', '~> 1.2'
    gem 'active_model_serializers'
    $ bundle install
    $ rails g serializer pet
    $ rails g serializer toy

    View Slide

  75. LET'S TRY IT OUT
    # Gemfile
    # gem 'jbuilder', '~> 1.2'
    gem 'active_model_serializers'
    $ bundle install
    $ rails g serializer pet
    $ rails g serializer toy

    View Slide

  76. LET'S TRY IT OUT
    # Gemfile
    # gem 'jbuilder', '~> 1.2'
    gem 'active_model_serializers'
    $ bundle install
    $ rails g serializer pet
    $ rails g serializer toy

    View Slide

  77. SERIALIZERS
    # app/serializers/pet_serializer.rb
    class PetSerializer < ActiveModel::Serializer
    attributes :id, :name, :created_at, :updated_at
    end
    # app/serializers/toy_serializer.rb
    class ToySerializer < ActiveModel::Serializer
    attributes :id, :name, :created_at, :updated_at
    end

    View Slide

  78. CONTROLLERS
    class PetsController < ApplicationController
    before_action :set_pet, only: [:show, :edit, :update, :destroy]
    # GET /pets
    # GET /pets.json
    def index
    render json: Pet.all
    end
    # GET /pets/1
    # GET /pets/1.json
    def show
    render json: @pet
    end
    .....
    end

    View Slide

  79. OUTPUT
    # /pets.json
    {"pets":
    [{"id":1, "name":"Cashew",
    "created_at":"2013-08-02T04:02:58.467Z",
    "updated_at":"2013-08-02T04:02:58.467Z"},
    {"id":2, "name":"Ginkgo",
    "created_at":"2013-08-02T04:03:06.958Z",
    "updated_at":"2013-08-02T04:03:06.958Z"}]}
    # /pets/2.json
    {"pet":
    {"id":2, "name":"Ginkgo",
    "created_at":"2013-08-02T04:03:06.958Z",
    "updated_at":"2013-08-02T04:03:06.958Z"}}
    JSON

    View Slide

  80. OUTPUT
    # /pets.json
    {"pets":
    [{"id":1, "name":"Cashew",
    "created_at":"2013-08-02T04:02:58.467Z",
    "updated_at":"2013-08-02T04:02:58.467Z"},
    {"id":2, "name":"Ginkgo",
    "created_at":"2013-08-02T04:03:06.958Z",
    "updated_at":"2013-08-02T04:03:06.958Z"}]}
    # /pets/2.json
    {"pet":
    {"id":2, "name":"Ginkgo",
    "created_at":"2013-08-02T04:03:06.958Z",
    "updated_at":"2013-08-02T04:03:06.958Z"}}
    JSON

    View Slide

  81. OUTPUT
    # /pets.json
    {"pets":
    [{"id":1, "name":"Cashew",
    "created_at":"2013-08-02T04:02:58.467Z",
    "updated_at":"2013-08-02T04:02:58.467Z"},
    {"id":2, "name":"Ginkgo",
    "created_at":"2013-08-02T04:03:06.958Z",
    "updated_at":"2013-08-02T04:03:06.958Z"}]}
    # /pets/2.json
    {"pet":
    {"id":2, "name":"Ginkgo",
    "created_at":"2013-08-02T04:03:06.958Z",
    "updated_at":"2013-08-02T04:03:06.958Z"}}
    "THAT'S BETTER, BUT..."
    JSON

    View Slide

  82. CUSTOM FIELDS
    # app/serializers/base_serializer.rb
    class PetSerializer < ActiveModel::Serializer
    attributes :id, :created_at, :updated_at
    # allow custom inclusion of a single field
    def include_created_at?
    if @options.key?(:fields)
    return @options[:fields][:created_at]
    end
    true
    end
    end

    View Slide

  83. CUSTOM FIELDS
    # app/serializers/base_serializer.rb
    class PetSerializer < ActiveModel::Serializer
    attributes :id, :created_at, :updated_at
    # allow custom inclusion of a single field
    def include_created_at?
    if @options.key?(:fields)
    return @options[:fields][:created_at]
    end
    true
    end
    end
    "THAT'S BETTER, BUT..."

    View Slide

  84. BASE SERIALIZER
    # app/serializers/base_serializer.rb
    class BaseSerializer < ActiveModel::Serializer
    attributes :id, :created_at, :updated_at
    end
    # app/serializers/pet_serializer.rb
    class PetSerializer < BaseSerializer
    attributes :name
    end
    # app/serializers/toy_serializer.rb
    class ToySerializer < BaseSerializer
    attributes :name
    end

    View Slide

  85. CUSTOM FIELDS +
    # app/serializers/base_serializer.rb
    class BaseSerializer < ActiveModel::Serializer
    attributes :id, :created_at, :updated_at
    # allow custom inclusion of ANY field
    # via options passed in from the controller
    def include?(field)
    if @options.key?(:fields)
    return @options[:fields][field]
    end
    super(field)
    end
    end

    View Slide

  86. ONE-TO-MANY
    # /app/models/pet.rb
    class Pet < ActiveRecord::Base
    has_many :toys
    end
    # app/serializers/pet_serializer.rb
    class PetSerializer < BaseSerializer
    attributes :name
    has_many :toys
    end

    View Slide

  87. OUTPUT
    JSON
    # /pets/2.json
    {"pet":
    {"id":2,
    "name":"Ginkgo",
    "toys":[
    {"id":3,
    "name":"Busy Bee"},
    {"id":4,
    "name":"Dan's sock"}]}}

    View Slide

  88. ONE-TO-ONE
    # /app/models/pet.rb
    class Pet < ActiveRecord::Base
    has_many :toys
    belongs_to :person
    end
    # app/serializers/pet_serializer.rb
    class PetSerializer < BaseSerializer
    attributes :name
    has_many :toys
    has_one :person
    end

    View Slide

  89. OUTPUT
    JSON
    # /pets/2.json
    {"pet":
    {"id":2,
    "name":"Ginkgo",
    "toys":[
    {"id":3, "name":"Busy Bee"},
    {"id":4, "name":"Dan's sock"}]
    "person": {"id":1,"name":"Dan"}}}

    View Slide

  90. OUTPUT
    JSON
    # /pets.json
    {"pets":
    [{"id":1,
    "name":"Cashew",
    "toys":[...],
    "person":{"id":1,"name":"Dan"}},
    {"id":2,
    "name":"Ginkgo",
    "toys":[...],
    "person":{"id":1,"name":"Dan"}}]}

    View Slide

  91. OUTPUT
    JSON
    # /pets.json
    {"pets":
    [{"id":1,
    "name":"Cashew",
    "toys":[...],
    "person":{"id":1,"name":"Dan"}},
    {"id":2,
    "name":"Ginkgo",
    "toys":[...],
    "person":{"id":1,"name":"Dan"}}]}
    "DUPLICATION!"

    View Slide

  92. BASE SERIALIZER
    # app/serializers/base_serializer.rb
    class BaseSerializer < ActiveModel::Serializer
    # sideload related data by default
    embed :ids, include: true
    attributes :id, :created_at, :updated_at
    end

    View Slide

  93. OUTPUT
    JSON
    # /pets.json
    {"toys":[
    {"id":1, "name":"BTVRuby catnip"},
    {"id":2, "name":"Mr Smoochums"},
    {"id":3, "name":"Busy Bee"},
    {"id":4, "name":"Dan's sock"}],
    "people":[
    {"id":1, "name":"Dan"}],
    "pets":[
    {"id":1,
    "name":"Cashew",
    "toy_ids":[1,2],
    "person_id":1},
    {"id":2,
    "name":"Ginkgo",
    "toy_ids":[3,4],
    "person_id":1}]}

    View Slide

  94. OUTPUT
    JSON
    # /pets.json
    {"toys":[
    {"id":1, "name":"BTVRuby catnip"},
    {"id":2, "name":"Mr Smoochums"},
    {"id":3, "name":"Busy Bee"},
    {"id":4, "name":"Dan's sock"}],
    "people":[
    {"id":1, "name":"Dan"}],
    "pets":[
    {"id":1,
    "name":"Cashew",
    "toy_ids":[1,2],
    "person_id":1},
    {"id":2,
    "name":"Ginkgo",
    "toy_ids":[3,4],
    "person_id":1}]}

    View Slide

  95. OUTPUT
    JSON
    # /pets.json
    {"toys":[
    {"id":1, "name":"BTVRuby catnip"},
    {"id":2, "name":"Mr Smoochums"},
    {"id":3, "name":"Busy Bee"},
    {"id":4, "name":"Dan's sock"}],
    "people":[
    {"id":1, "name":"Dan"}],
    "pets":[
    {"id":1,
    "name":"Cashew",
    "toy_ids":[1,2],
    "person_id":1},
    {"id":2,
    "name":"Ginkgo",
    "toy_ids":[3,4],
    "person_id":1}]}

    View Slide

  96. OUTPUT
    JSON
    # /pets.json
    {"toys":[
    {"id":1, "name":"BTVRuby catnip"},
    {"id":2, "name":"Mr Smoochums"},
    {"id":3, "name":"Busy Bee"},
    {"id":4, "name":"Dan's sock"}],
    "people":[
    {"id":1, "name":"Dan"}],
    "pets":[
    {"id":1,
    "name":"Cashew",
    "toy_ids":[1,2],
    "person_id":1},
    {"id":2,
    "name":"Ginkgo",
    "toy_ids":[3,4],
    "person_id":1}]}
    "NO DUPLICATION...
    I APPROVE."

    View Slide

  97. API DESIGN

    View Slide

  98. FILTERING DATA
    /pets/cats
    /pets/dogs

    View Slide

  99. FILTERING DATA
    /pets/cats
    /pets/dogs
    ~ VS. ~
    /pets?type=cats
    /pets?type=dogs

    View Slide

  100. FILTERING DATA
    /pets/cats
    /pets/dogs
    ~ VS. ~
    /pets?type=cats
    /pets?type=dogs
    /pets?type=cats,dogs

    View Slide

  101. FILTERING DATA
    /pets/cats
    /pets/dogs
    ~ VS. ~
    /pets?type=cats
    /pets?type=dogs
    /pets?type=cats,dogs
    /pets?type=cats,dogs&color=red

    View Slide

  102. INCLUDING RELATIONSHIPS
    Best guess inclusions
    (e.g. always include toys with pets)

    View Slide

  103. INCLUDING RELATIONSHIPS
    /pets?include=toys,people
    Best guess inclusions
    (e.g. always include toys with pets)
    ~ VS. ~
    No inclusions by default.
    Custom inclusions as requested.

    View Slide

  104. EMBEDDING RELATIONSHIPS
    Always embed any related data
    (i.e. JBuilder approach)

    View Slide

  105. EMBEDDING RELATIONSHIPS
    Always embed any related data
    (i.e. JBuilder approach)
    ~ VS. ~
    Favor "side-loading" and embed data very
    selectively (and only when it won't be shared)

    View Slide

  106. VERSIONING
    One version that's never broken

    View Slide

  107. VERSIONING
    One version that's never broken
    ~ VS. ~
    Version by host or path
    api.rubypets.org
    v1.api.rubypets.org
    rubypets.org/api/v1

    View Slide

  108. SPARSE FIELDSETS
    Include all fields

    View Slide

  109. SPARSE FIELDSETS
    Include all fields
    ~ VS. ~
    Allow for custom inclusion of fields:
    /pets?fields=id,name,age

    View Slide

  110. SPARSE FIELDSETS
    Include all fields
    ~ VS. ~
    Allow for custom inclusion of fields:
    /pets?fields=id,name,age
    /pets?include=toys,people&
    pet_fields=id,name,age&
    people_fields=id,name&
    toy_fields=name

    View Slide

  111. SORTING
    Default sort order only

    View Slide

  112. SORTING
    Default sort order only
    ~ VS. ~
    Allow for custom sort order:
    /pets?sort=name # ascending
    /pets?sort=-name # descending

    View Slide

  113. PAGINATION
    No limits on resources

    View Slide

  114. PAGINATION
    No limits on resources
    ~ VS. ~
    Provide a default page size, a max page size,
    and allow custom requests:
    /pets?page=2&per_page=100

    View Slide

  115. PAGINATION
    No limits on resources
    ~ VS. ~
    Provide a default page size, a max page size,
    and allow custom requests:
    /pets?page=2&per_page=100
    Opinion! JSON API should separate the primary resource from
    its related resources when "side-loading" in order to clarify issues
    of primacy like pagination.

    View Slide

  116. PAGINATION, CONT.
    As per RFC 5988, return a LINK header to
    allow for navigation:
    Link: ; rel="next",
    ; rel="last"
    Possible values for `rel`:
    first, last, next, prev

    View Slide

  117. TL;DR
    A Hypermedia API makes full use of HTTP and a
    hypermedia content type to be as as navigable as
    the web itself.
    REST was a term originally invented by Roy
    Fielding, but since most "REST" APIs aren't in line
    with his definition, the term "Hypermedia API" was
    created.
    HYPERMEDIA APIS

    View Slide

  118. HYPERMEDIA APIS
    Designing Hypermedia APIs
    by Steve Klabnik
    designinghypermediaapis.com
    Recommendation:
    It's not TL, so there's no reason for DR !

    View Slide

  119. HYPERMEDIA APIS
    "Hypermedia designs promote scalability, allow resilience
    towards future changes, and promote decoupling and
    encapsulation, with all the benefits those things bring. On the
    downside, it is not necessarily the most latency-tolerant
    design, and caches can get stale if you’re not careful. It may not
    be as efficient on an individual request level as other designs."
    Steve Klabnik
    Designing Hypermedia APIs
    designinghypermediaapis.com

    View Slide

  120. SECURITY

    View Slide

  121. SECURITY
    A Brief History of OAuth

    View Slide

  122. SECURITY
    A Brief History of OAuth

    View Slide

  123. *BASIC* SECURITY
    RECOMMENDATIONS
    • Restrict access to particular users (e.g. non-admins) as
    needed in your controller and serializer layers

    View Slide

  124. *BASIC* SECURITY
    RECOMMENDATIONS
    • Restrict access to particular users (e.g. non-admins) as
    needed in your controller and serializer layers
    • HTTPS for all the things

    View Slide

  125. *BASIC* SECURITY
    RECOMMENDATIONS
    • Restrict access to particular users (e.g. non-admins) as
    needed in your controller and serializer layers
    • HTTPS for all the things
    • Use token-based security

    View Slide

  126. *BASIC* SECURITY
    RECOMMENDATIONS
    • Restrict access to particular users (e.g. non-admins) as
    needed in your controller and serializer layers
    • HTTPS for all the things
    • Use token-based security
    • If you want to build an OAuth provider, don't
    go it alone. Use a gem like James Coglan's
    oauth2-provider:
    https://github.com/songkick/oauth2-provider

    View Slide

  127. View Slide

  128. THANKS!
    @dgeb
    Burlington Ruby 2013

    View Slide