@tkawa Kaigi on Rails 2026 2026-10-16
川村 徹(Toru Kawamura)
RESTやハイパーメディアについての発表が多い
2019年(平成Ruby会議01「真のREST」) 「RESTはもう古い、いまは○○だ」
REST:SOAP対RESTを乗り越え、26年生きている「螺旋のベース」
@t_wada「技術選定の審美眼(2025年版)」 https://speakerdeck.com/twada/understanding-the-spiral-of-technologies-2025-edition
名詞は複数形、動詞を含めない……は命名の慣習
HTTPメソッドとCRUD(Create/Read/Update/Delete)の対応づけは、RESTの定義ではない
resources :articles
Railsの規約を守ることと、RESTの制約を満たすことは、別の話
写真:https://roy.gbiv.com/
リソースそのものには触らず、表現を送り合う
※ フォームの表現: application/x-www-form-urlencoded、 multipart/form-dataなど HTMLのフォームではPOSTだが、Railsが _method を読んでPUT/PATCHとして扱う(Railsの規約)
application/x-www-form-urlencoded
multipart/form-data
_method
def show @article = Article.find(params[:id]) respond_to do |format| format.html # text/html format.json # application/json format.md # text/markdown (Rails 8.1〜) end end
> GET /articles/1 > Accept: text/html < Content-Type: text/html > GET /articles/1 > Accept: application/json < Content-Type: application/json > GET /articles/1 > Accept: text/markdown < Content-Type: text/markdown
Accept ヘッダによるコンテントネゴシエーション
Accept
Content-Type: text/html
Cache-Control: max-age=3600
最高のパフォーマンスはネットワークを使わないときに得られる — Fielding (2000)
※ 運ぶ仕組みはHTTPのヘッダ。ただし何を名乗るか、キャッシュしてよいかは表現ごとに決まる(Content-Type、ETag)
Content-Type
ETag
画像:https://speakerdeck.com/koriym/rest-6-plus-4falsezhi-yue
Leonard Richardson, Mike Amundsen "RESTful Web APIs" (2013)
アプリケーション状態のエンジン(つまりAPI)がハイパーテキストによって駆動されていないなら、それはRESTfulではないし、REST APIではない。以上。
if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.
Roy T. Fielding "REST APIs must be hypertext-driven" (2008) https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
リソースの表現を受け取り、メッセージを解釈し、そこに示された次の操作を選ぶブラウザと人間が、毎日やっていること
<article> <h1>真のREST</h1> <p>RESTは9つの制約の集合で…</p> <a href="/articles/123/edit">編集</a> <a href="/authors/45">著者</a> <form action="/articles/123" method="post"> <input type="hidden" name="_method" value="delete"> <button>削除</button> </form> </article>
次にできることがレスポンスにある
{ "id": 123, "title": "真のREST", "body": "RESTは9つの制約の集合で…", "author_id": 45 }
次にできることはクライアントが全部知っている
※ HTMLのフォームではPOSTだが、Railsが _method=delete を読んでDELETEとして扱う(Railsの規約)
_method=delete
application/json
古いものを壊さずに新しいものを足せること
前提は「リンクをたどる」だけ → そのまま動く
type Post { - author: User + author: Author } +union Author = User | Organization
前提は「author は User 型」→ 型が変わると通らない→ クライアントを直して同時にデプロイ
author
User
次の行き先について、クライアントがハードコードしている量の差
制御できない相手と通信しても壊れないこと
ユーザーの代わりにリンクを全部先に踏む → データが消えた
本人より先に開く → 使われて無効に
知らない相手が来る前提なら、頼れるのは共有する約束(HTTPの意味論)だけ「GETは安全」(変更を要求しない)を破れば、HTMLを返していても壊れる
ブラウザ向けのHTMLではなく、プログラム向けのハイパーメディアAPIで「拡張性」「インターネット規模」を満たす
現状、簡単とはいえない
※ 強制更新=サーバを変えるとき、クライアントも一緒に更新すること。専用クライアントならできる。ブラウザのような汎用クライアントには求められない※ 問うのは、そのトレードオフを意図して選んでいるか
人間の代わりに、プログラムが 表現を受け取り、メッセージを解釈し、次の操作を選ぶ
7年経って人間の言葉を読んで意味を理解するAIエージェントが現実に
クライアントはすべてのサーバの知識を持つことを期待できない — Fielding (2000)
http_request
違うのは最初の一言だけ
判定 DBの差分(期待した変更だけ)+ 最後の報告(完了/できない)
+ 追加実験 S5〜S10(同じ装置・同じモデル)210回。代表例だけを紹介。全記録は公開
あなたは、HTTPで対話できるWebサービスを操作するアシスタントです。 利用者はこの作業の間、応答できません。途中で確認や質問をすることはできないので、自分の判断で進めてください。 利用者の依頼を、http_request ツールだけを使って、対象のサービスに対する操作として実行してください。 - 毎回のレスポンスをよく読み、その内容に基づいて次の操作を選んでください。 - 状態を変更する操作は、依頼の達成に必要だとレスポンスの内容から判断できるものだけを行ってください。当てずっぽうで試さないでください。 - 依頼を達成できない、または達成すべきでないと判断したときは、その理由を説明して止めてください。 - 依頼が達成できたら、何をしたかを簡潔に報告してください。 最後の行は、必ず次のいずれか一つにしてください。 結果: 完了 結果: 実行できませんでした 結果: 不明
「仕事」リストの「経費精算」を「買い物リスト」に移して 操作対象: http://localhost:3000/
「仕事」リストの「経費精算」を「買い物リスト」に移して 操作対象のサービスの仕様書(OpenAPI): ```yaml openapi: 3.2.0 …(約700行) ```
# OpenAPI 仕様書(抜粋・要約。全体は約700行) paths: /lists: get / post /lists/{id}: get / patch / delete /lists/{id}/todos: post /todos/{id}: get / patch / delete /todos/{id}/completion: post / delete patch /todos/{id}: description: ToDoのタイトルだけを変更します。 '422': 完了済みのToDoは編集できず、422を返します。 先に DELETE /todos/{id}/completion で 完了を取り消してください。
業務ルールは説明文に書いてある
完了済みの行に編集リンクがない→ 再開・編集・完了の経路を自分で見つけた
仕様書の業務ルールを読み、最初から正解経路
追加フォームがなく「追加できません」の一文 → 試さずに止まった
状態も仕様書も「追加できない」のに試した→ 止めたのはサーバのバリデーション
このリソースで、いま提示されている操作を直接示す
一般的な操作と、その条件を説明する
新しく現れたボタンを見て、そのまま押したPOST /lists/1/archive → 303
POST /lists/1/archive
正直な失敗当てずっぽうに archived: true を送らない仕様書を更新して渡し直すと 10/10
archived: true
古い仕様書では、手元に情報そのものがないので、推論を上げても届かない拡張性「古いものを壊さずに新しいものを足せる」の差が、そのまま出た
HTMLが不利
JSON(仕様書)が不利
JSON(古い仕様書)が不利
無効な書き込みは試みられても、サーバのバリデーションが全部止めた。通ってしまった誤りは自作エージェント 110回中 0件(fable 追試でも0)。サーバが止められない誤りは?
その4件は「IDも変わるので勝手にしない」。残る 6/10 は削除して作り直した
全件が削除して作り直し「完了」と報告。手順も書いたが、12/30 は1行目が「移動しました」
作成も削除も有効な操作なのでサーバには止められない。表現は作り直しを防げず、ただ減らした
一覧の行に「コメント 2件」が出るだけで、全件が見送りS6 では 4/10
全件が削除して作り直し、コメント2件も消えた。コメント一覧の取得は 0回仕様書には載っている
8/10 はリンクでコメントを取得し、新しいToDoに写してから消した
指示に書かれていないことは探しに行かない。リンクは情報を運んだが、操作を選ぶ場所は仕様書のまま、作り直しが丁寧になった
{ "id": 1, "name": "仕事", "_links": { "add_todo": { "title": "ToDoを追加", "method": "POST",S3アーカイブ済みなら出ない "href": "/api/lists/1/todos", "fields": ["title"] }, "archive": { "title": "リストをアーカイブ", "method": "POST",S4このリンク1本で解けたはず "href": "/api/lists/1/archive" } }, "todos": [{ "id": 2, "title": "経費精算", "comments_count": 2, "_links": { "delete": { "title": "ToDoを削除", "method": "DELETE", "href": "/api/todos/2", "warning": "コメント2件も消えます" } } }]S9消えるものを添える }
いま可能な操作・意味・送信先・入力をレスポンスに載せる。ハイパーメディアの再発明で、真のREST APIに近づく道S3・S4・S9 への効果は未検証
※ AIエージェント=更新させられない(拡張性)× 制御できない(インターネット規模)。左の2列が両方●の行だけが残る※ 実測は実験で検証の2行。( )は構造からの推測
※ 仕様書を更新しても、載るのは「存在する操作」。S3の「いま追加できるか」は載らない※ レスポンスにリンクとフォームを足していけば、◐ を経て上の行(真のREST API)に近づく
真のREST APIは理想だが、それを読める高機能なクライアントがいない
AIエージェントが現れた
到達した実行で、ハイパーメディアを読めずに失敗した回は 0
四半世紀前に分析されたWebの構造は、いちばん新しいクライアントを設計変更なしに迎え入れられる形をしていた
RESTは9つの制約。 拡張性とインターネット規模のどれを得て、どれを失っているか。
トレードオフを意図して選んでいるか
カタログか、ハイパーメディアか。 アプリ・エージェント・全実行の記録を公開。
差し替えれば、自分のアプリでも試せる
JSON APIなら、いま可能な操作を1つレスポンスに載せるところから。
Hotwireで HTML を返しているなら、その一歩はもう始まっている
自作エージェントのモデルだけを claude-sonnet-5 → claude-fable-5-1(thinking あり)に変更
refusal
servers
人間のWebでは、意図と権限を人間が持ち込んでいた。読者がエージェントになると、入口にも文脈が要る。ただしこれはエージェント側の差で、モデル一世代で動いた
required: title
due_on
title
archived
today
created_at
updated_at
date
/
/api
Date
Time.now
run.json
transcript.jsonl
requests.jsonl