RailsでシンプルなREST APIを設計する
業務のRailsサーバーサイドのAPIリファクタ対応を通して得られた知見について、前回はService Objectについて書いたけれど、今回はREST API周りの手法について書きたい。
全体的にRails wayに沿って、実装者の恣意性による方針のブレを極力排した、見通しの良いリソースベースの設計を実現することを意識している。
REST APIのエンドポイント設計
CRUDの基本アクションの使用を優先し、適宜リソースを分割する
コントローラが元々持っているRESTアクションやデフォルトの5つの機能にはないメソッドを付け加えたいと思ったら、いつだって新しいコントローラを作る。それだけでいいのです。
上の記事でDHHが述べているように、index、show、new、edit、create、update、destroyの基本アクションの利用を優先し、独自アクションの定義を避けることは、リソースベースのエンドポイント設計の方針を明確化し、controllerの処理の見通しを良くする。
その一方で、DBのテーブルと1対1でcontrollerのリソースを定義している場合は、基本アクションのみで現実のビジネス要件に応えることはすぐに難しくなるだろう。DHHは、DBのテーブルとの対応にこだわらずにcontrollerのリソースを分割することで、基本アクションのみで実装できるとしている。
個人的には、厳密に基本アクションだけ使うのがベストかどうかは何とも言えない気がするけれど、DBのテーブルとの1対1にこだわらずにcontrollerのリソース分割を考えて良い、という考え方については、リソース設計の柔軟性がグッと増して視界が開けるような感覚があった。
resourcesメソッドの利用
上の基本アクションの利用と関連して、ルーティングの設定にresourcesメソッドを利用するとroutes.rbの記述がシンプルで見通しが良くなり、かつ実装者の恣意性によってエンドポイント定義のルールの一貫性が損なわれることがない。
resourcesメソッドをネストしてcontrollerを別立てする手法も便利だ。簡単な例で言うと、
このようにroutingを書くと、
Prefix | Verb | URI Pattern | Controller#Action |
---|---|---|---|
posts | GET | /posts(.:format) | posts#index |
user_posts | GET | /users/:user_id/posts(.:format) | users/posts#index |
rails routesで出力される定義がこうなり、
namespaceの分離
上のresourcesメソッドをネストするテクニックと併せて、namespaceメソッドを利用することでも、同じリソースについて完全に分離された複数のエンドポイント定義、controllerの実装を持つことができる。
例えばユーザーアプリケーション向けのAPIと管理画面向けのAPIが同じコードベースで実装されている場合に、管理画面向けのAPIに dashboard
namespaceを割り振って、影響範囲を分離した上で別の体系のAPIとして実装する、というような使い方ができる。
Mastodonのroutes.rbを見ると、Railsのroutingの様々な手法が博覧会的に用いられていて参考になる。
総じて、業務でのREST APIの活用を考えると、多重実装的な考え方を許容してでも、文脈ごとにリソースを分けて影響範囲を分離し、個別の要件に柔軟に応えていくような考え方が持続可能な開発につながると感じている。
JSONレスポンスのフォーマットを統一する
業務のリファクタ対象がJSONを返すタイプのAPI(フロントエンドはReact)だったため、JSONレスポンスのフォーマットが実装者依存で無秩序にならないようにするため、下記のような手法を導入してみた。
ActiveModelSerializersを利用する
リファクタ前に使われていたjbuilderのレスポンス定義が規則性のない無秩序状態でつらすぎたので、ActiveModelSerializersを導入してみたところ、当初の予想以上に快適に使えて驚いた。
ActiveModelSerializersは上のサンプル実装例のように、Railsのmodelに対応するserializerを用意して、Railsのmodelと同じようにhas_many
やbelongs_to
でアソシエーションを定義できる。このserializerをcontrollerで指定するだけで、モデル間のアソシエーションを含むレスポンスについても、ActiveModelSerializers側で決まったフォーマットで返すことができるようになる。
基本的なユースケースが如何にもスマートに見える一方で、複雑なビジネス要件に対する柔軟性が低いのではないかと気になっていたけれど、実際には
- 1つのRails modelに対して、複数のserializer classを定義して、任意に使い分けることができる
- 例えばnamespaceごとに別のserializerにするなど
- controllerでincludeオプションを指定することで、定義されたアソシエーションのうち、どの範囲をレスポンスに含めるか、エンドポイントごとに任意に指定できる
- 任意のオプションを作成して、controllerから渡した値を元にプロパティの出し分けを制御したりもできる
など、痒いところに手が届く機能が実装されていて、非常に柔軟に活用できるライブラリだった。
逆に苦手なユースケースとしては、ActiveModelSerializersを使った場合には配列形式のJSONレスポンスの件数が多くなるほどパフォーマンスが悪くなるので、大規模なデータを返す必要がある場合には他の方法を検討した方が良さそうだ。
エラーハンドリングの方式とJSONフォーマットの統一
リファクタ前のプロダクトはサーバーサイドのエラーハンドリングとレスポンスフォーマットの方式がバラバラで、その結果としてフロントエンドでもまともにエラーメッセージの出し分けができていないなど、悲惨な状態だったので、上のような形で、エラーメッセージと、class名を変換したエラーコードを返すシンプルなフォーマットでまずは統一させてみた。
世の中のWebアプリケーションの実装を見ると、Railsでのエラーハンドリングの方法とJSONのエラーレスポンスの返し方には様々な手法がある印象で、別のアプリの設計に携わる機会があればまた改めて深く考えてみたいと感じている。
おわりに
- リソースを文脈ごとに分割して、見通しの良いエンドポイントを設計する
- レスポンスフォーマットの決定について、実装者の恣意性によるバラつきを排除できる仕組みを導入する
全体を通して、これらの観点が見えて来たように思う。
リファクタ業務を通じて、RailsでのREST API設計については一定知見のようなものができてきたけれど、REST APIはビジネス要件に柔軟に応えるために、リソースとエンドポイントを分割して個別対応するという、多重実装を許容するようなアプローチを取らざるを得ない点に融通の利かなさを感じる部分はあり、この点でGraphQLの活用が近年注目されているのかなと思ったりもした。GraphQLでのAPI設計にも機会があればぜひ関わってみたいと感じている。