50
HYPERMEDIA: THE MISSING ELEMENT to Building Adaptable Web APIs in Rails Toru Kawamura @tkawa RubyKaigi 2014 ハイパーメディア: RailsでWeb APIをつくるには、これが足りない

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

Embed Size (px)

DESCRIPTION

RubyKaigi 2014 http://rubykaigi.org/2014/presentation/S-ToruKawamura Japanese enlargement version http://www.slideshare.net/tkawa1/rubykaigi2014-hypermedia-the-missing-element-enlarged-ja

Citation preview

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

HYPERMEDIA: THE MISSING ELEMENT

to Building Adaptable Web APIs in Rails

Toru Kawamura @tkawa

!RubyKaigi 2014

ハイパーメディア: RailsでWeb APIをつくるには、これが足りない

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

@tkawaToru 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”

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

Web API

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

“Web”

http://www.opte.org/the-internet/

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

http://pixabay.com/en/spider-web-net-grid-silk-drops-13516/

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

https://www.flickr.com/photos/tamaki/260594564/

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

• 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

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

http://www.slideshare.net/yohei/webapi-36871915

“Whether an API should be RESTful or not depends on the requirement”

– 「WebAPIのこれまでとこれから」by @yohei

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

http://pixabay.com/en/spider-web-net-grid-silk-drops-13516/

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

Change

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

Change is inevitable !

Web APIs must adapt to changes

変化は避けられない Web APIは変化に適応しなければならない

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

Two types of Change

With versioning Without versioning

Incompatible Compatible

Breaks clients Does not break clients

Breaking Change Non-Breaking Change

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

Breaking Changes are Harmful

• Terrible user experience

• Forces client developers to rewrite/redeploy code

• What if on …

壊す変更は有害

ひどいユーザ体験

クライアント開発者にコードの書き直し・再デプロイを強いる

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

With versioning Without versioning

Incompatible Compatible

Breaks clients Does not break clients

Breaking Change Non-Breaking Change

Because of what?なぜ起こるの?

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

Many clients are built from human-readable documentation

GET /v1/statuses?id=#{id} GET /v1/statuses?id=#{id}

人間が読める説明書から作られるクライアントがたくさんある

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

GET /v2/statuses/#{id} GET /v1/statuses?id=#{id}×Need to rewrite code

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

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": [ { "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": [

GET /v1/statuses?id=#{id} GET /v1/statuses?id=#{id}

機械が読める説明書から作られるクライアントもある

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

{ "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": [

GET /v2/statuses/#{id} GET /v1/statuses?id=#{id}×Need to regenerate code

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

{ uber: { version: "1.0", data: [{ url: "http://www.ishuran.dev/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"

• API changes should be reflected in clients

• It is good to split up explanations of the API and embed them into each API response

• A lot of assumptions about the API make a tight coupling

Because of Coupling密結合のせい

APIの変更がクライアントに反映されるべき

APIの説明を分割して各レスポンスに埋め込むのが良い

APIについての多大な仮定は密結合を生む

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

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

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

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)

• Server knows what the next FizzBuzz will be

• Client wants all FizzBuzz from one to the last in order

例で見る疎結合

サーバは100までの数のFizzBuzzを計算できる

サーバは次のFizzBuzzが何になるか知っている

クライアントは1から最後まで順番にすべてのFizzBuzzが欲しい

http://sef.kloninger.com/posts/201205fizzbuzz-for-

managers.html

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

Coupled client

• Every URL and parameter is hardcoded

• Duplicates the server logic such as counting up

"/v2/fizzbuzz/#{i}"

(1..1000)

(1..100).each do |i| answer = HTTP.get("/v1/fizzbuzz?number=#{i}") puts answer end

密結合なクライアント

すべてのURLとパラメータがハードコードされている

カウントアップのようなサーバロジックと同じことをやっている

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

Decoupled client

• No hardcoded URLs

• Client doesn’t break when changing URLs / the restriction

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

疎結合なクライアント

ハードコードされたURLなし

URLや条件を変えてもクライアントは壊れない

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

The “API Call” metaphor is dangerous

• We need to move away from the paradigm where a client arranges a URL and parameters in advance and calls API (like RPC…)

• What a client does next should be to choose from links in the response == HYPERMEDIA

「APIコール」のメタファーは危険

URLとパラメータを用意してAPIを呼ぶというRPCのようなパラダイムから離れよう

クライアントが次にすることはリンクから選ぶことこれがハイパーメディア

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

This is not imaginary but already present in HTML

これは想像上のものではなく、すでにHTMLにある

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

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

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

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

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

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

各画面は次に何ができるかのリンクやフォームの「メニュー」を含み、ブラウザはその中から選ぶ

これがハイパーメディア

ハイパーメディアはワークフローを示す

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

One more hint in a Crawler

• Crawlers follow links and can submit some forms • Crawlers understand the data in an HTML document

and their “meaning” • How can they do that?

クローラーにはもう1つヒントが

クローラはHTMLの中のデータと意味を理解しているどうやって?https://support.google.com/webmasters/answer/99170

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

• 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<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>

URLに結びつけることで大まかな「データの意味」も表す

Microdataは構造化データをHTMLに埋め込むしくみ

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

• 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)

<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>

schema.org is the standard vocabulary promoted by

Bing, Google, Yahoo! and Yandex

Microdata

http://getschema.org/index.php/Main_Page

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

You could build a Web API in HTML

• “Microdata DOM API” allows clients to extract data from HTMLhttp://www.w3.org/TR/microdata/#using-the-microdata-dom-api

• Available in JavaScript: https://github.com/termi/Microdata-JS

• There are also some specs for translating Microdata into JSON

• HTML’s great advantage is that it has links and forms built-in

var user = document.getItems('http://schema.org/Person')[0]; var name = user.properties['name'][0].itemValue; alert('Hello ' + name + '!');

HTMLでWeb APIを作ることもできる

Microdata DOM APIでHTMLからデータを抽出できる

MicrodataからJSONに変換もできる

HTMLはリンクとフォームを持っているのが大きなアドバンテージ

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

But you probably want aJSON Web API…

• You have to fill in links and forms (also the meanings of data, if possible)

data link form

HTML+Microdata ✓✓ ✓ ✓

JSON ✓ - -

✓✓: including “meaning of data”

でもたぶんJSON Web APIが欲しいよね

リンクとフォームを埋めればいい(できればデータの意味も)

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

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 ✓ ✓ ✓✓✓: including “meaning of data”

リンクとフォームを表現できるJSONベースのフォーマットがある

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

A Solution

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

Hypermicrodata gem

• 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

https://github.com/tkawa/hypermicrodata

サーバサイドでHTMLをJSONに変換

MicrodataだけではなくリンクとフォームもHTMLから抽出

データの意味も表しやすい形でJSONベースのフォーマットを生成

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

Design procedure in Rails with Hypermicrodata gem

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(Then, write profiles and explanations that are not defined in schema.org, if necessary)

Hypermicrodata gemを使ったRailsによる設計手順

1. リソース設計

2. 状態遷移図を描く

3. データの名前を対応するURLに結びつける

4. HTMLテンプレートを書きMicrodataでマークアップ

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

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: Notecontroller : NotesController

routing: resources :notes

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

2. Draw a state diagram

Collection Memberitem

collection

create*†

update*, delete*

* unsafe † non-idempotent

Begin with Collection & Member Resource pattern of Rails (API ver.)

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

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

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

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

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

4. Write HTML templates with Microdata

/app/views/notes/index.html.haml

GET /notes HTTP/1.1 Host: www.example.com Accept: application/vnd.amundsen-uber+json

%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' = 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"} ] }] } }

Collection of Note

Link

Form

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

%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'

/app/views/notes/show.html.haml

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" } ] }] } }

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

%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

{ "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" } ] }] } }

= 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

explanation of restriction

{ "rel": "publish", "url": "/notes/1/publish", "action": "append" }, { "rel": "next", "url": "/notes/2" },

Now you can publish, but cannot go prev

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

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アプリと同じように考えることで状態遷移に着目しリンクとフォームを意識できる

「制約は自由をもたらす」

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

If you want to write only JSON, you should keep in mind

• 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

• Use a JSON-based format with links and forms

• In addition, it is better to use standard names such as schema.org

もしJSONだけを書くときは注意すること

リンク・フォームを意識するために状態遷移図を描きましょう

疎結合のために、model.to_jsonはやめてビューテンプレートを使いましょう

リンクとフォームを持ったJSONベースのフォーマットを使いましょう

schema.orgのような標準名を使うとさらに良いです

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

– 「Webを支える技術」@yohei

“WebアプリとWeb APIを分けて考えない”

“Don’t consider Web app and Web API separately”

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

Conclusion: Design Your Web API the same way

as an HTML Web App

• A Web API is nothing special, It just has a different representation format

• Awareness of state transitions by drawing a diagram will remind you of links and forms

結論: Web APIはHTML Webアプリと同じように設計しよう

Web APIは特別なものではなく、ただ表現フォーマットが違うだけ

状態遷移図を描いて状態遷移を意識することで、リンクやフォームを忘れずにすむ

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

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

• Hypermedia is one of the most important elements of REST, and a key step toward building Web APIs adaptable to change

残念ながら、デファクトスタンダードがない

RESTの制約・原則を意識するともっとうまくできる

ハイパーメディアはRESTの最も重要な要素で変化に適応できるWeb APIへの重要なステップ

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

Build a Better & Adaptable Web API. Thank you for your attention.

• 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

References