In Relentless Pursuit of Rest

In Relentless Pursuit of Rest

"That's not very RESTful." As a Rails developer you've probably heard or even spoken that proclamation before, but what does it really mean? What's so great about being RESTful anyway?

RESTful architecture can narrow the responsibilities of your Rails controllers and make follow-on refactorings more natural. In this talk, you'll learn to refactor code to follow RESTful principles and to identify the positive impact those changes have throughout your application stack.

C537a86fdc5234b3e941a84c154ba034?s=128

Derek Prior

April 26, 2017
Tweet

Transcript

  1. In Relentless Pursuit of REST

  2. Derek Prior @derekprior

  3. What is REST?

  4. That's not very RESTful

  5. HTTP

  6. Hypermedia

  7. HATEOAS

  8. It Doesn't Matter

  9. What Does Matter?

  10. Nouns

  11. Verbs

  12. Resources & Actions

  13. More Small Things

  14. None
  15. Custom Actions

  16. None
  17. resource :user, only: [:edit, :update] do get :edit_password, on: :member

    put :update_password, on: :member end
  18. Longhand Definitions get '/user/edit_password', to: 'users#edit_password' put '/user/update_password', to: 'users#update_password'

  19. Controller Mappings

  20. Controller Mappings • users#edit_password

  21. Controller Mappings • users#edit_password • users#update_password

  22. Controller Mappings • users#edit_password • users#update_password • noun#verb_noun

  23. Controller Mappings

  24. Controller Mappings • passwords#edit

  25. Controller Mappings • passwords#edit • passwords#update

  26. Controller Mappings • passwords#edit • passwords#update • noun#verb

  27. Controller Mappings Before After users#edit_password passwords#edit users#update_password passwords#update

  28. resource :user, only: [:edit, :update] do resource :password, only: [:edit,

    :update] end
  29. class PasswordsController < ApplicationController def edit # what used to

    be `users#edit_password` end def update # what used to be `users#update_password` end end
  30. None
  31. That's not worth another controller!

  32. Password isn't a model!

  33. Controllers do not need to map to ActiveRecord objects

  34. Complex Actions

  35. No one sets out to write complex controllers

  36. def update @photo = current_user.photos.find(params[:id]) if @photo.update(update_photo_params) redirect_to @photo else

    render :edit end end private def update_photo_params params.require(:photo).permit(:caption) end
  37. As a user, I want to mark a photo as

    featured so it is displayed prominently on my profile.
  38. As a user, I want to mark a photo as

    featured so it is displayed prominently on my profile. • Oh, and I should only be allowed to have one featured photo
  39. As a user, I want to mark a photo as

    featured so it is displayed prominently on my profile. • Oh, and I should only be allowed to have one featured photo • Oh, and if I un-feature a photo, my first photo should automatically be featured
  40. def update @photo = current_user.photos.find(params[:id]) @photo.assign_attributes(update_photo_params) if @photo.valid? && update_photo(photo)

    redirect_to @photo else render :edit end end private def update_photo(photo) ApplicationRecord.transaction do if photo.featured_changed? && photo.featured? current_user.photos.featured.update!(featured: false) elsif photo.featured_changed? && !photo.featured? current_user.photos.first.update!(featured: true) end photo.save! end end
  41. def update_photo(photo) ApplicationRecord.transaction do if photo.featured_changed? && photo.featured? current_user.photos.featured.update!(featured: false)

    elsif photo.featured_changed? && !photo.featured? current_user.photos.first.update!(featured: true) end photo.save! end end
  42. def update @photo = current_user.photos.find(params[:id]) @photo.assign_attributes(update_photo_params) if @photo.valid? && update_photo(photo)

    redirect_to @photo else render :edit end end private def update_photo(photo) ApplicationRecord.transaction do if photo.featured_changed? && photo.featured? current_user.photos.featured.update!(featured: false) elsif photo.featured_changed? && !photo.featured? current_user.photos.first.update!(featured: true) end photo.save! end end
  43. Differing Perspectives

  44. resources :photos, only: [:new, :edit, :create, :update] do resource :featured_flag,

    only: [:create, :destroy] end
  45. class FeaturedFlagsController < ApplicationController def create @photo = current_user.photos.find(params[:photo_id]) if

    AddFeaturedFlag.to(@photo) redirect_to photos_path else redirect_to photos_path, error: t('.error') end end def destroy @photo = current_user.photos.find(params[:photo_id]) if RemoveFeaturedFlag.from(@photo) redirect_to photos_path else redirect_to photos_path, error: t('.error') end end end
  46. def create @photo = current_user.photos.find(params[:photo_id]) if AddFeaturedFlag.to(@photo) redirect_to photos_path else

    redirect_to photos_path, error: t('.error') end end
  47. def destroy @photo = current_user.photos.find(params[:photo_id]) if RemoveFeaturedFlag.from(@photo) redirect_to photos_path else

    redirect_to photos_path, error: t('.error') end end
  48. class AddFeaturedFlag def self.to(photo) new(photo).call end def initialize(photo) @photo =

    photo end def call ApplicationRecord.transaction do @photo.user.photos.featured.update!(featured: false) @photo.update!(featured: true) end end end
  49. Be Boring

  50. Ambiguous Language

  51. resources :orders do put :process, on: :member end

  52. None
  53. None
  54. Language Matters

  55. Noun: Order Verb: process

  56. class OrdersController < ApplicationController before_action :find_order, except: [:index] before_action #...

    before_action #... before_action :ensure_processable, only: [:process] before_action #... # a bunch of actions def process # procedure for processing an order # potentially very complicated order.update!(process_params) end # more actions private # dozens of private methods useful to various actions end
  57. Noun: Order Verb: ship

  58. resources :orders do put :ship, on: :member end

  59. resources :orders do resource :shipment, only: [:create] end

  60. class ShipmentsController < ApplicationController before_action :ensure_processable def create # procedure

    for processing an order # potentially very complicated order.update!(shipment_params) end private # a subset of those dozens of private methods end
  61. None
  62. I see duplication!

  63. You didn't improve the code!

  64. Breathing Room

  65. None
  66. None
  67. State Machines

  68. State Machines on Rails

  69. resources :orders do member do put :authorize put :capture put

    :refund put :reject end end
  70. state_machine initial: :pending do before_transition captured: :refunded, do: :ensure_refundable after_transition

    captured: :refunded, do: :process_refund after_transition authorized: :captured, do: :capture_payment after_transition pending: :rejected, do: :void_order after_transition pending: :authorized, do: :authorize_payment #... end
  71. state_machine initial: :pending do # callbacks... event :authorize do transition

    pending: :authorized end event :capture do transition authorized: :captured end event :refund do transition captured: :refunded end event :reject do transition all => :rejected end end
  72. state_machine initial: :pending do # callbacks... # events... state :captured

    do validates :reward_points, presence: true end state :rejected do validate :validates_tax_exemption end end
  73. state_machine initial: :pending do before_transition captured: :refunded, do: :ensure_refundable after_transition

    captured: :refunded, do: :process_refund after_transition authorized: :captured, do: :capture_payment after_transition pending: :rejected, do: :void_order after_transition pending: :authorized, do: :authorize_payment event :authorize do transition pending: :authorized end event :capture do transition authorized: :captured end event :refund do transition captured: :refunded end event :reject do transition all => :rejected end state :captured do validates :reward_points, presence: true end state :pending do validate :validates_tax_emption end end
  74. None
  75. What if update didn't exist?

  76. resources :orders, only: [:new, :create] do resource :authorization, only: [:create]

    resource :capture, only: [:create] resource :refund, only: [:create] resource :rejection, only: [:create] end
  77. class Authorization def initialize(order) @order = order end def process

    raise unless authorizable? ApplicationRecord.transaction do @order.update!(state: "authorized") PaymentGateway.authorize(@order.payment_method, @order.total) end end private def authorizable? # Is the order in a state where it can be authorized? end end
  78. Outside In

  79. None
  80. This is fine for CRUD apps, but...

  81. Imagination

  82. Be CRUDdy

  83. Be Boring

  84. Be RESTful

  85. Questions?

  86. Derek Prior • twitter: @derekprior • email: derek@thoughtbot.com • podcast:

    http://bikeshed.fm