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. 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 ...
  2. 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 ...
  3. 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 ...
  4. 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 ...
  5. 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 ...
  6. 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 ...
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. LET'S TRY IT OUT # Gemfile # gem 'jbuilder', '~>

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

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

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

    1.2' gem 'active_model_serializers' $ bundle install $ rails g serializer pet $ rails g serializer toy
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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..."
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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"}}}
  30. 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!"
  31. 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
  32. 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}]}
  33. 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}]}
  34. 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}]}
  35. 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."
  36. 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.
  37. 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)
  38. VERSIONING One version that's never broken ~ VS. ~ Version

    by host or path api.rubypets.org v1.api.rubypets.org rubypets.org/api/v1
  39. SPARSE FIELDSETS Include all fields ~ VS. ~ Allow for

    custom inclusion of fields: /pets?fields=id,name,age
  40. 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
  41. SORTING Default sort order only ~ VS. ~ Allow for

    custom sort order: /pets?sort=name # ascending /pets?sort=-name # descending
  42. 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
  43. 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.
  44. 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
  45. 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
  46. 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
  47. *BASIC* SECURITY RECOMMENDATIONS • Restrict access to particular users (e.g.

    non-admins) as needed in your controller and serializer layers
  48. *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
  49. *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
  50. *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