Weitere ähnliche Inhalte Ähnlich wie Hypermedia: The Missing Element to Building Adaptable Web APIs in Rails (増補日本語版) (20) Mehr von Toru Kawamura (7) Hypermedia: The Missing Element to Building Adaptable Web APIs in Rails (増補日本語版)1. HYPERMEDIA:
THE MISSING ELEMENT
to Building Adaptable Web APIs in Rails
ハイパーメディア: RailsでWeb APIをつくるには、これが足りない
Toru Kawamura
@tkawa
!
RubyKaigi 2014
RESTful Web APIs 読書会 #19 2014.10.09
2. @tkawa
Toru Kawamura
• フリーランス Ruby/Rails プログラマ
• Technology Assistance Partner at
SonicGarden Inc.
• REST厨 (RESTafarian)
inspired by Yohei Yamamoto (@yohei)
• Sendagaya.rb 共同主催
• “RESTful Web APIs”
読書会主催
7. • プライベート
• 内部から使われる
• SPAや専用の
クライアントが使う
• だいたい予想できる
コントロールできる
• パブリック
• 外部から使われる
• 汎用的な目的の
クライアントが使う
• 予想しづらい
コントロールしづらい
18. 機械が読める説明書から作られる
クライアントもある
{
"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": [
19. GET /v2/statuses/#{id} GET /v1/statuses?id=#{id} ×コードの再生成が必要
{
"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": [
20. {
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"
21. {
uber: {
「密結合」が原因
version: "1.0",
data: [{
url: "http://www.ishuran.dev/notes/1",
name: "Article",
data: [
• APIの変更がクライアントに反映されればよい
{
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の説明を分割して各レスポンスに埋め込む
のが良い方法
• APIについての多大な仮定は密結合を生む
23. 例で見る疎結合: FizzBuzzaaS
• by Stephen Mizell
http://fizzbuzzaas.herokuapp.com/
http://smizell.com/weblog/2014/solving-fizzbuzz-with-hypermedia
• サーバは与えられた100までの数の
FizzBuzzを計算できる
• サーバは次のFizzBuzzが何になるか知っ
ている
• クライアントは1から最後まで順番にす
http://sef.kloninger.com/posts/ べてのFizzBuzzが欲しい
201205fizzbuzz-for-managers.
html
24. 密結合なクライアント
answer = HTTP.get("/v1/fizzbuzz?number=#{i}")
puts answer
end
"/v2/fizzbuzz/#{i}"
(1..1000)
(1..100).each do |i|
• すべてのURLとパラメータがハードコードされている
• カウントアップのような、サーバロジックと同じこと
をクライアントでも行っている
25. 疎結合なクライアント
root = HTTP.get_root
answer = root.link('first').follow
puts answer
while answer.link('next').present?
answer = answer.link('next').follow
puts answer
end next リンクが重要
• ハードコードされたURLなし
• URLや条件を変えてもクライアントは壊れ
ない
26. • 実際にリンクをたど
る代わりに、
埋め込みリソースを
使うことで
リクエスト数を減ら
すことが可能
http://techlife.cookpad.com/entry/2014/09/08/093000
29. HTMLのWeb
• WebアプリやWebサイ
トはずっと変わり続け
ているが、ブラウザは
壊れていない
• HTMLのWebでは、なぜ
HTMLのリンクブラウザは壊れない?
http://www.youtypeitwepostit.com/messages
33. もしHTMLにリンクがなかったら?
代わりにワークフロー手順書がある?
• 各WebアプリごとにURLやパラ
メータをハードコードした専用
クライアントを作りたくなる
• クライアントとサーバのコード
が密結合して、変更できない
Webアプリになってしまう
• これが今のWeb APIがやってい
ること
メッセージWebアプリ利用の手順書
1. アドレスバーに /messages と入力し
て GET
2. アドレスは /messages のまま、 title
と body のパラメータに文字列を
セットして POST
3. message-id を受け取って、アドレス
バーに /messages/{message-id} と入
力して GET
36. 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>
• 構造化データをHTMLドキュメントに埋め込むしくみ
• データを変えることなくドキュメントの構造を変えられる
• データをURLに結びつけることで、大まかな「データの意
味」も表す(これもリンクの一種)
37. 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="http://www.example.com" itemprop="url">www.example.com</a>
I live in Albuquerque, 標準語彙NM (and
ボキャブラリー)
work as an <span itemprop="at <span itemprop="by Bing, affiliation">Google, title">Yahoo! engineer</ACME Corp</and span>
span>.
Yandex
</div>
• 構造化データをHTMLドキュメントに埋め込むしくみ
• データを変えることなくドキュメントの構造を変えられる
• データをURLに結びつけることで、大まかな「データの意
味」も表す(これもリンクの一種)
http://getschema.org/index.php/Main_Page
39. 変化に適応するために必要なもの
data link form
HTML - ✓ ✓
HTML
+Microdata ✓✓ ✓ ✓
• APIには構造化データが必要
✓✓: 「データの意味」を含む
• 柔軟なワークフローにはリンクとフォームが必要
40. 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” でHTMLからデータを抽出できる
http://www.w3.org/TR/microdata/#using-the-microdata-dom-api
• JavaScriptの実装: https://github.com/termi/Microdata-JS
• MicrodataからJSONに変換する仕様もいくつかある
• HTMLはリンクとフォームを持っているのが大きなアドバンテージ
41. でもきっとJSON Web APIが欲しいはず
data link form
HTML
+Microdata ✓✓ ✓ ✓
JSON ✓ - -
✓✓: 「データの意味」を含む
• リンクとフォームを埋めればいい
(できればデータの意味も)
42. JSONでリンク・フォームを表す
• リンクやフォームを表
現できるJSONベースの
フォーマットを使う
• 他にも
Siren, Collection+JSON,
Mason, Verbose など…
data link form
JSON ✓ - -
JSON
+Link header ✓ ✓ -
HAL ✓ ✓ -
JSON-LD ✓✓ ✓ -
JSON-LD
+Hydra ✓✓ ✓ ✓
UBER ✓ ✓ ✓
✓✓: 「データの意味」を含む
43. JSONでデータの意味を表す:
「プロファイル」
• ALPSプロファイル
• MicrodataをHTML以外のどんなフォーマットにも適用
可能にする
• JSON-LDコンテキスト
• ドキュメントとコンテキストの両方を同じ1つの
フォーマットで扱える
46. class PeopleController < ApplicationController
before_action :set_message, only: %i(show edit update destroy)
include Hypermicrodata::Rails::HtmlBasedJsonRenderer
...
end
.person{itemscope: true, itemtype: 'http://schema.org/Person',
itemid: person_url(@person), data: {main_item: true}}
.media
.media-image.pull-left
= image_tag @person.picture_path, alt: '', itemprop: 'image'
.media-body
%h1.media-heading
%span{itemprop: 'name'}= @person.name
= link_to 'collection', people_path, rel: 'collection'
Example in HAL (application/hal+json)
{
"image": "/assets/bob.png",
"name": "Bob Smith",
"isPartOf": "/people",
"_links": {
"self": { "href": "http://www.example.com/people/1" },
"type": { "href": "http://schema.org/Person" },
"collection": { "href": "/people" },
"profile": { "href": "/assets/person.alps" }
}
}
47. Hypermicrodata gemを使った
Railsによる設計手順
1. リソース設計
2. 状態遷移図を描く
3. データの名前を対応するURLに結びつける
4. HTMLテンプレート(Haml, Slimなど)を書いて、Microdata
でマークアップする
(その後、必要ならschema.org定義にないプロファイルと説明を書く)
49. 1. リソース設計
カラム名説明タイプ
text noteの内容のテキストtext
published_at noteの公開時間datetime
(id, created_at, updated_at) (自動生成)
$ rails g model Note text:text published_at:datetime
model: Note
controller: NotesController
routing: resources :notes
50. 2. 状態遷移図を描く
Railsの Collection & Member リソースパターンから始める (API ver.)
item
collection
Collection Member
create*†
update*, delete*
* 安全でない
† 冪等でない
51. Collection
of Note
Note
(text, published_at,
created_at,
updated_at, id)
item
collection
create*†
update*, delete*,
publish*
next, prev
Home
notes home
* 安全でない
† 冪等でない
52. 3. データの名前を対応するURLに
結びつける
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 (各noteは個別のURLを持つので不要)
Home http://schema.org/SiteNavigationElement
53. item IANA ‘item’ &
http://schema.org/hasPart
collection IANA ‘collection’ &
http://schema.org/isPartOf
notes -
create Activity Streams ‘create'
update Activity Streams ‘update’
delete Activity Streams ‘delete’
publish Activity Streams ‘post’
IANA registered Link Relation: http://www.iana.org/assignments/link-relations/
Activity Streams Verbs: http://activitystrea.ms/registry/verbs/
54. 4. HTMLテンプレートと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
55. %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" }
]
}]
}
}
56. %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": [
条件の表現
publishできるが
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" }
]
}]
}
}
58. メリット 1: DRY
• リンクとフォームをHTMLテンプレートに一度
書けば、JSON生成時にも再利用できる
• Microdataマークアップもそのまま再利用できる
• Bonus: HTMLのMicrodataはSEO効果を上げる
(JSONにも可能性あり)
59. メリット 2: リンクとフォームを
意識できる
• JSON Web APIを作るときには、状態遷移の重要
性を見落としやすい
• APIをHTMLのWebアプリのように表現すること
で、状態遷移に着目して適切にリンクとフォー
ムを実装できる
60. メリット 3: 制約
• 「HTMLドキュメントをMicrodataでマークアップし
て、それを一定のルールでフォーマットされた
JSONに変換する」という制約
• この制約はよりよい設計のガイド
• “Constraints are liberating” 「制約は自由をもたらす」
61. もしJSONだけを書くときは
注意すること
• リンク・フォームを意識するために:
• 状態遷移図を描きましょう
• APIを疎結合に保つために:
• model.to_json の代わりに Jbuilder/RABL のようなビューテンプレートや
リプレゼンターを使いましょう
• リンクとフォームを持ったJSONベースのフォーマットを使いましょう
• さらに schema.org のような標準名を使うとベター
63. 結論: Web APIはHTML Webアプリと
同じように設計しよう
• Web APIは特別なものではなく、ただ表現
フォーマットが違うだけ
• 状態遷移図を描いて状態遷移を意識すること
で、リンクやフォームを忘れずにすむ
65. よりよい、変化に適応できる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