Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Hypermedia: The Missing Element to Building Adaptable Web APIs in Rails

22,932 views

Published on

RubyKaigi 2014
http://rubykaigi.org/2014/presentation/S-ToruKawamura

Japanese enlargement version http://www.slideshare.net/tkawa1/rubykaigi2014-hypermedia-the-missing-element-enlarged-ja

Published in: Technology

Hypermedia: The Missing Element to Building Adaptable Web APIs in Rails

  1. 1. HYPERMEDIA: THE MISSING ELEMENT to Building Adaptable Web APIs in Rails ハイパーメディア: RailsでWeb APIをつくるには、これが足りない Toru Kawamura @tkawa ! RubyKaigi 2014
  2. 2. @tkawa Toru Kawamura • Freelance Ruby/Rails programmer • Technology Assistance Partner at SonicGarden Inc. • RESTafarian inspired by Yohei Yamamoto (@yohei) • Co-organizer of Sendagaya.rb • Organizer of the reading group of “RESTful Web APIs”
  3. 3. Web API
  4. 4. “Web” http://www.opte.org/the-internet/
  5. 5. http://pixabay.com/en/spider-web-net-grid-silk-drops-13516/
  6. 6. https://www.flickr.com/photos/tamaki/260594564/
  7. 7. • Private • For internal use • For SPA or dedicated clients only • Almost expected, almost controllable • Public • For external use • For general-purpose clients • Less expected, less controllable
  8. 8. “Whether an API should be RESTful or not depends on the requirement” – 「WebAPIのこれまでとこれから」by @yohei http://www.slideshare.net/yohei/webapi-36871915
  9. 9. http://pixabay.com/en/spider-web-net-grid-silk-drops-13516/
  10. 10. Change
  11. 11. Change is inevitable ! Web APIs must adapt to changes 変化は避けられない Web APIは変化に適応しなければならない
  12. 12. Two types of Change With versioning Without versioning Incompatible Compatible Breaks clients Does not break clients Breaking Change Non-Breaking Change
  13. 13. Breaking Changes are Harmful 壊す変更は有害 • Terrible user experience ひどいユーザ体験 • Forces client developers to rewrite/redeploy code クライアント開発者にコードの書き直し・再デプロイを強いる • What if on …
  14. 14. Because of what? なぜ起こるの? With versioning Without versioning Incompatible Compatible Breaks clients Does not break clients Breaking Change Non-Breaking Change
  15. 15. Many clients are built from human-readable documentation 人間が読める説明書から作られる クライアントがたくさんある GET /v1/statuses?id=#{id} GET /v1/statuses?id=#{id}
  16. 16. GET /v2/statuses/#{id} GET /v1/statuses?id=#{id} ×Need to rewrite code
  17. 17. Some clients are built from machine-readable documentation { "apiVersion": "1.0.0", "basePath": "http:// petstore.swagger.wordnik.com/api", "resourcePath": "/store", "produces": [ "application/json" ], "apis": [ { "path": "/store/order/{orderId}", "operations": [ { GET /v1/statuses?id=#{id} GET /v1/statuses?id=#{id} "method": "GET", "summary": "Find purchase order by ID", "notes": "For valid response try integer IDs with value <= 5. Anything above 5 or nonintegers will generate API errors", "type": "Order", "nickname": "getOrderById", "authorizations": {}, "parameters": [ 機械が読める説明書から作られる クライアントもある
  18. 18. GET /v2/statuses/#{id} GET /v1/statuses?id=#{id} ×Need to regenerate code { "apiVersion": "2.0.0", "basePath": "http:// petstore.swagger.wordnik.com/api", "resourcePath": "/store", "produces": [ "application/json" ], "apis": [ { "path": "/store/order/{orderId}", "operations": [ { "method": "GET", "summary": "Find purchase order by ID", "notes": "For valid response try integer IDs with value <= 5. Anything above 5 or nonintegers will generate API errors", "type": "Order", "nickname": "getOrderById", "authorizations": {}, "parameters": [
  19. 19. { uber: { Because of Coupling version: "1.0", data: [{ url: "http://www.ishuran.dev/notes/1", name: "Article", data: [ • API { name: changes "articleBody", should be reflected in clients value: "First note's text" }, { It is name: • value: }, good "datePublished", null to split up explanations of the API and { embed name: "dateCreated", value: "2014-them 09-11T12:into 00:31+09:00" each API response }, { name: "dateModified", value: "2014-09-11T12:00:31+09:00" • A }, { lot of assumptions about the API make a tight name: "isPartOf", rel: "collection", coupling url: "/notes" 密結合のせい APIの変更がクライアントに反映されるべき APIの説明を分割して各レスポンスに埋め込むのが良い APIについての多大な仮定は密結合を生む
  20. 20. With versioning Without versioning Incompatible Compatible Breaks clients Does not break clients Breaking Change Non-Breaking Change because of the Coupling because of the Decoupling
  21. 21. Decoupling in a example: FizzBuzzaaS • by Stephen Mizell 例で見る疎結合 http://fizzbuzzaas.herokuapp.com/ http://smizell.com/weblog/2014/solving-fizzbuzz-with-hypermedia • Server knows how to calculate FizzBuzz for given number (<= 100) サーバは100までの数のFizzBuzzを計算できる • Server knows what the next FizzBuzz will be サーバは次のFizzBuzzが何になるか知っている • Client wants all FizzBuzz from one to the last in order クライアントは1から最後まで順番にすべてのFizzBuzzが欲しい http://sef.kloninger.com/posts/ 201205fizzbuzz-for-managers. html
  22. 22. Coupled client 密結合なクライアント answer = HTTP.get("/v1/fizzbuzz?number=#{i}") puts answer end "/v2/fizzbuzz/#{i}" (1..1000) (1..100).each do |i| • Every URL and parameter is hardcoded • Duplicates the server logic such as counting up すべてのURLとパラメータがハードコードされている カウントアップのようなサーバロジックと同じことをやっている
  23. 23. Decoupled client 疎結合なクライアント root = HTTP.get_root answer = root.link('first').follow puts answer while answer.link('next').present? answer = answer.link('next').follow puts answer end Link ‘next’ is the key • No hardcoded URLs • Client doesn’t break when changing URLs / the restriction ハードコードされたURLなし URLや条件を変えてもクライアントは壊れない
  24. 24. The “API Call” metaphor is dangerous 「APIコール」のメタファーは危険 • We need to move away from the paradigm where a client arranges a URL and parameters in advance and calls API (like RPC…) URLとパラメータを用意してAPIを呼ぶというRPCのようなパラダイムから離れよう • What a client does next should be to choose from links in the response == HYPERMEDIA クライアントが次にすることはリンクから選ぶこと これがハイパーメディア
  25. 25. This is not imaginary but already present in HTML これは想像上のものではなく、すでにHTMLにある
  26. 26. The HTML Web • Web apps and websites have been changing constantly without breaking browsers • Why don’t browsers break on the HTML Web? There are links in HTML WebアプリやWebサイトはずっと変わり続けているけど ブラウザは壊れていないのはなぜ? http://www.youtypeitwepostit.com/messages
  27. 27. Workflow in HTML • Web app includes a (suggested) workflow • Workflow is represented by a sequence of screen transitions — Links and Forms Webアプリはワークフローを含む ワークフローは一連の画面遷移で表現される それはリンクとフォーム ”RESTful Web APIs” p.11 Figure 1-7
  28. 28. Hypermedia show the workflow ハイパーメディアはワークフローを示す • Each screen includes what a browser can do next through links and forms like a “menu” • A browser chooses from the “menu” to go to the next step • This is HYPERMEDIA and exactly what FizzBuzzaaS does 3 4 各画面は次に何ができるかのリンクやフォームの 「メニュー」を含み、ブラウザはその中から選ぶ これがハイパーメディア
  29. 29. One more hint in a Crawler クローラーにはもう1つヒントが • Crawlers follow links and can submit some forms • Crawlers understand the data in an HTML document and their “meaning” • How can they do that? クローラはHTMLの中のデータと意味を理解している https://support.google.com/webmasters/answer/99170 どうやって?
  30. 30. Microdata <div itemscope itemtype="http://schema.org/Person"> My name is <span itemprop="name">Bob Smith</span> but people call me <span itemprop="nickname">Smithy</span>. Here is my home page: <a href="http://www.example.com" itemprop="url">www.example.com</a> I live in Albuquerque, NM and work as an <span itemprop="title">engineer</span> at <span itemprop="affiliation">ACME Corp</span>. </div> • Mechanism that embeds structured data within an HTML document • Document structure can change without changing data • Connects data with a URL that roughly represents the “meaning of data” (this is also a kind of link) Microdataは構造化データをHTMLに埋め込むしくみ URLに結びつけることで大まかな「データの意味」も表す
  31. 31. Microdata <div itemscope itemtype="http://schema.org/Person"> My name is <span itemprop="name">Bob Smith</span> but people call me <span schema.itemprop="org nickname">Smithy</span>. Here is my home page: <a href="is the http://standard www.example.vocabulary com" itemprop="promoted url">www.example.I live in Albuquerque, NM and by com</a> work as an Bing, <span itemprop="Google, title">at <span itemprop="affiliation">Yahoo! engineer</span> ACME Corp</and Yandex span>. </div> • Mechanism that embeds structured data within an HTML document • Document structure can change without changing data • Connects data with a URL that roughly represents the “meaning of data” (this is also a kind of link) http://getschema.org/index.php/Main_Page
  32. 32. You could build a Web API in HTML HTMLでWeb APIを作ることもできる var user = document.getItems('http://schema.org/Person')[0]; var name = user.properties['name'][0].itemValue; alert('Hello ' + name + '!'); • “Microdata DOM API” allows clients to extract data from HTML http://www.w3.org/TR/microdata/#using-the-microdata-dom-api Microdata DOM APIでHTMLからデータを抽出できる • Available in JavaScript: https://github.com/termi/Microdata-JS • There are also some specs for translating Microdata into JSON MicrodataからJSONに変換もできる • HTML’s great advantage is that it has links and forms built-in HTMLはリンクとフォームを持っているのが大きなアドバンテージ
  33. 33. But you probably want a JSON Web API… でもたぶんJSON Web APIが欲しいよね data link form HTML +Microdata ✓✓ ✓ ✓ JSON ✓ - - ✓✓: including “meaning of data” • You have to fill in links and forms (also the meanings of data, if possible) リンクとフォームを埋めればいい(できればデータの意味も)
  34. 34. Links and Forms in JSON • Use a JSON-based format that can represent links and forms • There are other formats Siren, Collection+JSON, Mason, Verbose, etc data link form JSON ✓ - - JSON +Link header ✓ ✓ - HAL ✓ ✓ - JSON-LD ✓✓ ✓ - JSON-LD +Hydra ✓✓ ✓ ✓ UBER ✓ ✓ ✓ リンクとフォームを表現できる JSONベースのフォーマットがある ✓✓: including “meaning of data”
  35. 35. A Solution
  36. 36. Hypermicrodata gem https://github.com/tkawa/hypermicrodata • Translate HTML into JSON on Server-side • Extract not only Microdata but also links and forms from HTML • Generate a JSON-based format that naturally fits with an explanation of meaning of data サーバサイドでHTMLをJSONに変換 Microdataだけではなく リンクとフォームもHTMLから抽出 データの意味も表しやすい形で JSONベースのフォーマットを生成
  37. 37. Design procedure in Rails with Hypermicrodata gem Hypermicrodata gemを使ったRailsによる設計手順 1. Design resources 2. Draw a state diagram 3. Connect names of data with corresponding URLs 4. Write HTML templates (Haml, Slim, etc) with Microdata markup 1. リソース設計 2. 状態遷移図を描く 3. データの名前を対応するURLに結びつける 4. HTMLテンプレートを書き Microdataでマークアップ (Then, write profiles and explanations that are not defined in schema.org, if necessary)
  38. 38. 1. Design resources column name short description type text content text of note text published_at published time of note datetime (id, created_at, updated_at) (auto-generated) $ rails g model Note text:text published_at:datetime model: Note controller: NotesController routing: resources :notes
  39. 39. 2. Draw a state diagram Begin with Collection & Member Resource pattern of Rails (API ver.) item collection Collection Member create*† update*, delete* * unsafe † non-idempotent
  40. 40. Collection of Note Note (text, published_at, created_at, updated_at, id) item collection create*† update*, delete*, * unsafe † non-idempotent publish* next, prev Home notes home
  41. 41. 3. Connect names of data with corresponding URLs Collection of Note http://schema.org/ItemList Note http://schema.org/Article text http://schema.org/articleBody published_at http://schema.org/datePublished created_at http://schema.org/dateCreated updated_at http://schema.org/dateModified id (No need because each note has its own URL) Home http://schema.org/SiteNavigationElement
  42. 42. 4. Write HTML templates with Microdata Collection of Note %div{itemscope: true, itemtype: 'http://schema.org/ItemList', itemid: notes_url, data: {main_item: true}} - @notes.each do |note| = link_to note.text.truncate(20), note, rel: 'item', itemprop: 'hasPart' /app/views/notes/index.html.haml GET /notes HTTP/1.1 Host: www.example.com Accept: application/vnd.amundsen-uber+json = form_for Note.new do |f| = f.text_field :text = f.submit rel: 'create' { "uber": { "version": "1.0", "data": [{ "url": "http://www.example.com/notes", "name": "ItemList", "data": [ { "name": "hasPart", "rel": "item", "url": "/notes/1" }, { "name": "hasPart", "rel": "item", "url": "/notes/2" }, { "rel": "create", "url": "/notes", "action": "append", "model": "note%5Btext%5D={text}" }, { "rel": "profile", "url": "/assets/note.alps"} ] }] } } Link Form
  43. 43. %div{itemscope: true, itemtype: 'http://schema.org/Article', itemid: note_url(@note), data: {main_item: true}} /app/views/notes/show.html.haml %span{itemprop: 'articleBody'}= @note.text %span{itemprop: 'datePublished'}= @note.published_at %span{itemprop: 'dateCreated'}= @note.created_at %span{itemprop: 'dateModified'}= @note.updated_at = form_for @note, method: :put do |f| = f.text_field :text = f.submit rel: 'update' = button_to 'Destroy', @note, method: :delete, rel: 'delete' = button_to 'Publish', publish_note_path(@note), rel: 'publish' unless @note.published? = link_to 'Next note', note_path(@note.next), rel: 'next' if @note.next = link_to 'Prev note', note_path(@note.prev), rel: 'prev' if @note.prev = link_to 'Collection of Note', notes_path, rel: 'collection', itemprop: 'isPartOf' GET /notes/1 HTTP/1.1 Host: www.example.com Accept: application/vnd.amundsen-uber+json Note { "uber": { "version": "1.0", "data": [{ "url": "http://www.example.com/notes/1", "name": "Article", "data": [ { "name": "articleBody", "value": "First note's text" }, { "name": "datePublished", "value": null }, { "name": "dateCreated", "value": "2014-09-11T12:00:31+09:00" }, { "name": "dateModified", "value": "2014-09-11T12:00:31+09:00" }, { "name": "isPartOf", "rel": "collection", "url": "/notes" }, { "rel": "update", "url": "/notes/1", "action": "replace", "model": "note%5Btext%5D={text}" }, { "rel": "delete", "url": "/notes/1", "action": "remove" }, { "rel": "publish", "url": "/notes/1/publish", "action": "append" }, { "rel": "next", "url": "/notes/2" }, { "rel": "profile", "url": "/assets/note.alps" } ] }] } }
  44. 44. %div{itemscope: true, itemtype: 'http://schema.org/Article', itemid: note_url(@note), data: {main_item: true}} %span{itemprop: 'articleBody'}= @note.text %span{itemprop: 'datePublished'}= @note.published_at %span{itemprop: 'dateCreated'}= @note.created_at %span{itemprop: 'dateModified'}= @note.updated_at = form_for @note, method: :put do |f| = f.text_field :text = f.submit rel: 'update' = button_to 'Destroy', @note, method: :delete, rel: 'delete' = button_to 'Publish', publish_note_path(@note), rel: 'publish' unless @note.published? = link_to 'Next note', note_path(@note.next), rel: 'next' if @note.next = link_to 'Prev note', note_path(@note.prev), rel: 'prev' if @note.prev = link_to 'Collection of Note', notes_path, rel: 'collection', itemprop: 'isPartOf' Note = button_to 'Publish', publish_note_path(@note), = link_to 'Next note', note_path(@note.next), = link_to 'Prev note', note_path(@note.prev), { "uber": { rel: 'publish' unless @note.published? rel: 'next' if @note.next rel: 'prev' if @note.prev "version": "1.0", "data": [{ "url": "http://www.example.com/notes/1", "name": "Article", "data": [ explanation of restriction Now you can publish, but cannot go prev { "name": "articleBody", "value": "First note's text" }, { "name": "datePublished", "value": null }, { "name": "dateCreated", "value": "2014-09-11T12:00:31+09:00" }, { "{ rel": "name": "publish", "dateModified", "url": "value": "/"2014-notes/09-11T12:1/publish", 00:31+09:00" }, "{ action": "name": "isPartOf", "append" "rel": }, "collection", "url": "/notes" }, { "rel": "update", "url": "/notes/1", "action": "replace", { "rel": "model": "next", "note%5Btext%"url": 5D={text}" "/notes/}, 2" }, { "rel": "delete", "url": "/notes/1", "action": "remove" }, { "rel": "publish", "url": "/notes/1/publish", "action": "append" }, { "rel": "next", "url": "/notes/2" }, { "rel": "profile", "url": "/assets/note.alps" } ] }] } }
  45. 45. 3 Pros of this design procedure • DRY • When providing both HTML and JSON • Awareness of links and forms • Framing the API as an HTML Web app gets you focused on these state transition • Constraints • “Constraints are liberating” この設計手順の3つのメリット HTMLとJSON両方提供するならDRY APIをWebアプリと同じように考えることで状態遷移に着目し リンクとフォームを意識できる 「制約は自由をもたらす」
  46. 46. If you want to write only JSON, you should keep in mind もしJSONだけを書くときは注意すること • To stay focused on the link/form pattern: • Draw a state diagram リンク・フォームを意識するために 状態遷移図を描きましょう • To keep your API decoupled: • Use view templates or representers such as Jbuilder/RABL instead of model.to_json 疎結合のために、model.to_jsonはやめて ビューテンプレートを使いましょう • Use a JSON-based format with links and forms リンクとフォームを持ったJSONベースのフォーマットを使いましょう • In addition, it is better to use standard names such as schema.org schema.orgのような標準名を使うとさらに良いです
  47. 47. “WebアプリとWeb APIを分けて考えない” “Don’t consider Web app and Web API separately” – 「Webを支える技術」@yohei
  48. 48. Conclusion: Design Your Web API the same way as an HTML Web App 結論: Web APIはHTML Webアプリと同じように設計しよう • A Web API is nothing special, It just has a different representation format Web APIは特別なものではなく、ただ表現フォーマットが違うだけ • Awareness of state transitions by drawing a diagram will remind you of links and forms 状態遷移図を描いて状態遷移を意識することで、リンクやフォームを忘れずにすむ
  49. 49. Finally • Unfortunately, no de-facto standard JSON format, client implementations, libraries, etc 残念ながら、デファクトスタンダードがない • We can do better by focusing on the principles and constraints of REST RESTの制約・原則を意識するともっとうまくできる • Hypermedia is one of the most important elements of REST, and a key step toward building Web APIs adaptable to change ハイパーメディアはRESTの最も重要な要素で 変化に適応できるWeb APIへの重要なステップ
  50. 50. Build a Better & Adaptable Web API. Thank you for your attention. References • L. Richardson & M. Amundsen “RESTful Web APIs” (O’Reilly) • 山本陽平 “Webを支える技術” (技術評論社) • Designing for Reuse: Creating APIs for the Future http://www.oscon.com/oscon2014/public/schedule/detail/34922 • API Design Workshop 配布資料 http://events.layer7tech.com/tokyo-wrk • https://speakerdeck.com/zdne/robust-mobile-clients-v2 • http://www.slideshare.net/yohei/webapi-36871915 • http://smizell.com/weblog/2014/solving-fizzbuzz-with-hypermedia • 山口 徹 “Web API デザインの鉄則” WEB+DB PRESS Vol.82

×