「《REST思想》と《リソース指向》と《Webページ》を一緒にしてはいけない」についてです。
用語の問題などについてはきっちりJxckさんが指摘されているので、私は自分で復習しつつ思ったことを書いてみます。
なんかRESTって難しい…とはあんまり思ってほしくないんですが、少なくともRails使うとちゃんとしたことが楽にできますよ。
REST思想
本に対してCRUD操作を行うインタフェースとして、以下のようなものを提供します。
役割 method + URL 本一覧を取得 GET /books 一つの本を取得 GET /books/1 新しい本を追加 POST /books 既存の本を修正 PUT /books/1 既存の本を削除 DELETE /books/1
これはいわゆるWeb APIについて、ということかなと推測しました。RESTというのはAPIのプロトコルのことだと思われている傾向がありますが、そういうわけではありません。Web全体についてのもので、APIについてもWebアプリについても適用されるものです。
実はRESTでは「統一インターフェイス」の制約からメソッドについて規定されていますが、URLの形については特に規定されません(もちろんAddressabilityの面で重要であることは言うまでもありません)。なので実は/books
,/books/1
でなくてもいいのですが、これを規約(CoC)でズバッときれいに決めてしまったのがRailsのすごいところの1つです。
本の追加や削除を行う場合は、本情報をJSON形式でPOSTリクエストのボディとして送ります。
application/x-www-form-urlencoded
形式で送ることは避けるべきです。
(中略)
しかし、正常系・異常系ともにアプリケーション内で統一された形式を利用してください。
このへんは、表現のフォーマット(メディアタイプ)の話なので直接RESTと関係するわけではありません。対応できるなら複数のフォーマットに対応すると便利だけど、実装の手間を考えて1つのフォーマットに絞る、というのは十分ありえます。
Railsの場合、デフォルトでapplication/x-www-form-urlencoded
もJSONも扱えるので、実はそんなに手間でもありません。
リソース指向
本の情報は
/books
にあるとして、拡張子を変えることで、様々なフォーマットでデータを利用できるようにします。
例えば本の一覧をJSON形式で欲しければ/books.json
にリクエストし、csv形式で欲しければ、/books.csv
にリクエストします。
/books.xls
のようにバイナリを提供してもかまいません。
これこそリソースと表現(Representation)の違いですね。
「リソース」はURLで表されますが、抽象的な概念であって、データではないので、送ることができません。送られるのはすべて表現です。(すべてのデータは表現、と言い換えてもよい)
表現にどのフォーマットを使用しているかを表すのがHTTPのContent-Type
ヘッダで、どのフォーマットなら受け入れられると伝えるのがAccept
ヘッダです。
GET /books HTTP/1.1
Host: example.jp
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
[
{"name": "foobar"},
...
GET /books HTTP/1.1
Host: example.jp
Accept: text/html
HTTP/1.1 200 OK
Content-Type: text/html
<!DOCTYPE html>
<html>
<head>
...
HTTPにはフォーマットを使い分ける機能がすでにあったんですね。
ブラウザで試すときなど、URLで表したほうが便利なこともあるので、/books.json
などの拡張子的なURLもよく使われます。(Railsではデフォルトでどちらも対応)
ただし、たとえば.json
であってもapplication/json
とは限らず、application/ld+json
とかapplication/hal+json
とかほかのタイプもありうるので、拡張子の指定よりはAccept
,Content-Type
の指定のほうが正確といえるでしょう。
ということで、
/books.csv
に対する更新を行う場合は、はPOSTリクエストのボディとして何を送ればよいのでしょうか。CSV形式で良いのでしょうか。JSONでしょうか。
また、更新に失敗したときのエラーはどう返すべきでしょうか。もし/books.csv
にリクエストししたにも関わらず、JSON形式でエラーを返ってくることになると、クライアントは苦労するでしょう。
このへんの心配は、ちゃんと実装すれば杞憂ですし、Railsなら実装も比較的簡単です。
Webページ
(中略)
役割 URL 一覧画面を表示 GET /books 詳細画面を表示 GET /books/1 追加formを表示 GET /books/new 追加アクション† POST /books/new 編集formを表示 GET /books/1/edit 編集アクション† POST /books/1/edit 削除確認画面を表示(任意)† GET /books/1/delete 削除アクション† POST /books/1/delete
(引用注:†はRailsのデフォルトにはありません)
Railsを使いはじめるとこのnew
とかedit
ってのは何なんだ、と思ってしまうのですが、これも別に必須というわけではありません。例えば、一覧画面の中に追加フォームを入れてしまうというのは、よく見かけますね。
どちらがいいかは場合によるのですが、Railsでは別ページにするのがより一般的という考えから、このデフォルトになっているというわけです。
ある意味new
やedit
は、表現の種類の1つと考えられるかもしれません。
- 更新FormページのURLとPOST先のURLを同じにする
(中略)
URLを同じにすることで、CSSなどのリソースのパスや、リクエストパスに依存したコードを書きやすくなります。
相対パスを書くと/books
と/books/new
の違いでハマることがある、ということですね。これは少し経験があるけど、全体としては些細なことです。というかRailsなら相対パスを使わずに済みます。
問題だと思うこと
Railsのコントローラの、ほぼscaffoldのコードはこんな感じです。
def create
@book = Book.new(book_params)
respond_to do |format|
if @book.save
format.html {
# 302 Found (or 303 See Other)
redirect_to @book, notice: 'Book was successfully created.'
}
format.json {
# 201 Created
render action: 'show', status: :created, location: @book
}
else
...
end
end
end
respond_to
を使うと、ロジック部分をそのままにフォーマットの違いに対応できる!すごい!
というのは確かなんですが、なんでフォーマットによってステータスコードが違うの?
新規作成したら、ほんとは201 Createdを返したいんですが、ブラウザは201を受け取ると自動的にはリダイレクトしない。そこさえ同じなら、respond_to
すら書かなくてよくなるのに。
Webブラウザで必要とされる画面遷移が特殊なのかもしれないですが、画面遷移(状態遷移)こそHATEOASの重要な要素であってRESTでも大切なのに、歴史的な事情か、仕様や実装に足りない部分がある気がするんですよね。
そういう意味では、XHR(Ajax)のブレイクスルーはかなり大きかったですね。PUT/DELETEも可能になったり、ブラウザの機能すら補ってくれた。昔「Web ApplicationをRESTfulに近づけていくことで、Web Serviceとの垣根をなくす方向を目指したい」と言っていたのを思い出して懐かしい気分になりました。
読書会とミートアップのお知らせ
『RESTful Web APIs』という書籍の読書会を毎月第2・4木曜日に行っています。これはRailsや特定の言語にまったく依存しない内容です。RESTやWeb APIに興味のある方の参加をお待ちしています。
http://www.circleaf.com/groups/19
その著者のMike Amundsen氏の来日にあわせて、ミートアップを4/12 19:00から渋谷で開催します。こちらもよろしくお願いします。
http://sendagayarb.doorkeeper.jp/events/10103