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.

E01ec1de2f7783812d2235a6a9aaaeea?s=128

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
  2. DO YOU NEED AN API?

  3. CLIENT-SIDE JS APPLICATION?

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

  5. NATIVE APPLICATION?

  6. NATIVE APPLICATION? YOU NEED AN API

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

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

  9. SCRIPTABLE?

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

  11. SO DO YOU NEED AN API?

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

  13. LET'S BUILD SOMETHING THAT DOES

  14. RubyPets!

  15. RubyPets!

  16. EMBER UI

  17. ANDROID & IOS APPS EMBER UI

  18. ANDROID & IOS APPS EMBER UI LOLCATS INTEGRATION

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

    API
  20. "WE NEED A RESTFUL API"

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

  22. RESTFUL API BASICS

  23. Your application's NOUNS are modeled as RESOURCES

  24. OUR RESOURCES Pets

  25. OUR RESOURCES People

  26. Every RESOURCE has a URI

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

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

  29. Resources are acted upon with HTTP METHODS

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

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

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

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

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

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

    + MORE
  36. "WE NEED AN AMBITIOUS RESTFUL API!"

  37. DEFINE AMBITIOUS

  38. DEFINE AMBITIOUS •Flexible

  39. DEFINE AMBITIOUS •Flexible •Consistent

  40. DEFINE AMBITIOUS •Flexible •Consistent •Efficient

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

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

  43. IS THERE ANYTHING WE WON'T DO?

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

  45. OK, LET'S GET STARTED!

  46. OK, LET'S GET STARTED!

  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 ...
  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 ...
  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 ...
  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 ...
  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 ...
  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 ...
  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
  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
  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
  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
  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
  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
  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
  60. "CUSTOM" ISN'T ALWAYS A GOOD THING

  61. WE NEED MORE CONVENTION AND LESS CONFIGURATION

  62. None
  63. None
  64. None
  65. None
  66. None
  67. None
  68. None
  69. None
  70. None
  71. None
  72. None
  73. LET'S TRY IT OUT # Gemfile # gem 'jbuilder', '~>

    1.2' gem 'active_model_serializers'
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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..."
  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
  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
  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
  87. OUTPUT JSON # /pets/2.json {"pet": {"id":2, "name":"Ginkgo", "toys":[ {"id":3, "name":"Busy

    Bee"}, {"id":4, "name":"Dan's sock"}]}}
  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
  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"}}}
  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"}}]}
  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!"
  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
  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}]}
  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}]}
  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}]}
  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."
  97. API DESIGN

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

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

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

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

    /pets?type=cats,dogs&color=red
  102. INCLUDING RELATIONSHIPS Best guess inclusions (e.g. always include toys with

    pets)
  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.
  104. EMBEDDING RELATIONSHIPS Always embed any related data (i.e. JBuilder approach)

  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)
  106. VERSIONING One version that's never broken

  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
  108. SPARSE FIELDSETS Include all fields

  109. SPARSE FIELDSETS Include all fields ~ VS. ~ Allow for

    custom inclusion of fields: /pets?fields=id,name,age
  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
  111. SORTING Default sort order only

  112. SORTING Default sort order only ~ VS. ~ Allow for

    custom sort order: /pets?sort=name # ascending /pets?sort=-name # descending
  113. PAGINATION No limits on resources

  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
  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.
  116. PAGINATION, CONT. As per RFC 5988, return a LINK header

    to allow for navigation: Link: <https://v1.api.rubypets.org/pets?page=3&per_page=100>; rel="next", <https://v1.api.rubypets.org/pets?page=9&per_page=100>; rel="last" Possible values for `rel`: first, last, next, prev
  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
  118. HYPERMEDIA APIS Designing Hypermedia APIs by Steve Klabnik designinghypermediaapis.com Recommendation:

    It's not TL, so there's no reason for DR !
  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
  120. SECURITY

  121. SECURITY A Brief History of OAuth

  122. SECURITY A Brief History of OAuth

  123. *BASIC* SECURITY RECOMMENDATIONS • Restrict access to particular users (e.g.

    non-admins) as needed in your controller and serializer layers
  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
  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
  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
  127. None
  128. THANKS! @dgeb Burlington Ruby 2013