KitchHike Tech Blog

KitchHike Product, Design and Engineering Teams

API開発で「意図せぬレスポンス」問題を防ぐ2つのコツ

こんにちは。KitchHikeエンジニアの小川です。 KitchHikeのアプリがつい先日リリースされました! 私は主にサーバサイドのAPI設計・開発を担当していたのですが、今回はその中で特に気を配った、意図しないレスポンスを防ぐためのAPI設計の取り組みについてご紹介したいと思います。

なお、本エントリはFood Service Engineers Meetup #3第6回スタートアップRails勉強会にて発表した内容を整理したものです。併せてご参照いただければと思います。

意図せぬレスポンス問題

API開発でよくぶつかる問題のひとつは、APIのレスポンスに意図しないデータ項目が含まれてしまうことです。たとえば user リソースをレスポンスとして返すケースを考えてみましょう。

Railsの場合、最もお手軽にjsonレスポンスを返すなら as_json でしょう。ここはひとつ、何も考えずにレスポンスを返してみましょう。

render json: @user.as_json

# => response
# {
#   "id": 1,
#   "first_name": "hike",
#   "last_name": "kitch",
#   ...
#   "password_digest": "xxxxxxxxx"
# }

なんとレスポンスに password_digest が含まれています! これはどう考えてもまずいです。

このように、非公開であるべきリソースがAPIレスポンスとして返されている状態は、公開APIであれば直ちにセキュリティ上の脆弱性となります。また、アプリ用の非公開APIであっても、クライアントにJSON形式等でデータが渡る以上、必ず避けるべきでしょう。

一方でこうした「意図せぬレスポンス」はちょっとしたミスにより発生しやすいものでもあります。

上記のケースは流石に極端ですが、たとえば「知らない間にDBのテーブルにカラムが追加されていた」ようなケースを想像してみると、容易に発生しうるものと考えられるのではないでしょうか。

意図せぬレスポンス問題の2つのパターン

この「意図せぬレスポンス」問題をもう少し深掘りすると、大体2つのパターンに分けることができます。

  1. そもそも公開してはいけないレスポンス項目を含めてしまった
  2. 特定のユーザー向けのレスポンス項目の公開範囲を誤った

以下ではこの2つの「意図せぬレスポンス」パターンに対して、KitchHikeがどのような設計アプローチで対処したかを説明していきます。

問題その1. そもそも公開してはいけないレスポンス項目を含めてしまった

この問題は、先ほどの as_json の実装例がよく示しています。

render json: @user.as_json

# => response
# {
#   "id": 1,
#   "first_name": "hike",
#   "last_name": "kitch",
#   ...
#   "password_digest": "xxxxxxxxx"
# }

jsonのシリアライズ時に、自分の意図しない、公開をしてはいけないレスポンス項目まで返してしまったパターンです。

好きこのんで、 password_digest をレスポンスに含めたい開発者はいないはずです。この問題の根本にあるのは、データ項目の非表示指定がブラックリスト方式だという点にあります。

render json: @user.as_json(except: [:address, :phone_number, ...])

as_json はデフォルトでモデルの全ての項目を返します。「この項目をレスポンスに含めたくない」という場合は、上記コードのようにそのことを明示的に指定しないと レスポンス項目に含めてしまいます。 したがって、開発者にモデルのどの項目を出さないべきかの漏れや認識違いがあれば、それが即レスポンスに含まれてしまうという、ミスが重大化しやすい問題をはらんでいるのです。

as_json ばかり槍玉にあげていますが、このことはブラックリスト方式を採用するシリアライズのライブラリであれば、言語問わず全てに言えることです。

問題その1の対策: レスポンスはホワイトリスト方式で指定する

こうした意図しないデータ項目のレスポンスを防ぐには、ブラックリスト方式の逆、ホワイトリスト方式でのレスポンス項目指定が効果的です。

ホワイトリスト方式での指定は、明示的にレスポンス項目を指定しない限り、レスポンスにその項目が含まれることはありません。したがって、仮にミスがあったとしても、それが公開されることはない、フェールセーフな仕組みとなるのです。

ホワイトリスト方式での指定をするgemには、たとえば以下のようなものがあります。

  • JbuilderRabl のようなテンプレートエンジン
  • ActiveModelSerializer

KitchHikeでは ActiveModelSerializer を採用しました。コード例はこんな感じです。

class UserSerializer < ActiveModel::Serializer
  attributes :id, :full_name

  def full_name
    "#{object.first_name} #{object.last_name}"
  end
end

# => response
# {
#   "id": 1,
#   "full_name": "hike kitch"
# }

ActiveModelSerializer では attributes にレスポンスに含めたい項目を指定します。指定していない項目はレスポンスには出てきません。これなら安心ですね!

このエントリでは深入りしませんが、 ActiveModelSerializer は記述がテンプレートエンジンのようなDSLではなく、Rubyコードなので、実装時は非常にテストがしやすく、また、 Concerns などRubyコードの抽象化プラクティスをそのまま適用できるという点で非常に使いやすいものでした。

補足: as_json のonly指定について

ちなみに as_json のコード例をご覧になって、 only を使えばいいのでは?と思った方がいらっしゃるかもしれません。

これは全くその通りです。 as_jsononly オプションでホワイトリスト方式の指定が可能です。

render json: @user.as_json(only: [:id, :first_name, :last_name, ...])

ですので、もし既に as_json を使っている場合でも、only を活用することで意図しないレスポンスに効果的に対処することが可能です。

ただ、個人的には as_json は、モデル側でオーバーライドした場合や、ネストしたモデルを扱う場合にやっぱりミスが起こりやすいと感じています。また、オプションを指定しないと全ての項目を返す時点で、根本において as_json はブラックリスト方式です。

したがって、選択の余地があるのならば、わざわざ as_json を使う理由はないと思っています。なお、この辺りの辛みについては、Thoughtbot社のエントリ “Better serialization, less as_json”によくまとまっているので、ぜひご参照ください。

問題その2. 特定のユーザー向けのレスポンス項目の公開範囲を誤った

この問題は、例えばログインしているユーザー自身にしか返してはいけない非公開のデータ項目が、誤って全てのユーザー向けのレスポンスに含めてしまった、というようなケースです。

よくあるパターンとしては、以下のようにクエリパラメータでレスポンスを出し分けるような場合でしょうか。

# /kitchens/:id
class KitchensController < ApplicationController
  def show
    if params[:scope] == "self"
       # ログインしているユーザー向けのレスポンス
    else
       # 全てのユーザー向けのレスポンス
    end
  end
end

上記コード例には、動作上、何の問題もありません。しかし、気になるのは条件分岐の箇所です。もしこの箇所にバグを作り込んでしまったら、ログインしているユーザー向けのレスポンスが誤って全てのユーザー向けのレスポンスとして返されてしまうかもしれません。

条件分岐のちょっとしたミスが、セキュリティ問題に直結してしまうというのは、コードの作りとして非常に脆く感じます。条件分岐の代替となる、より安全な出し分け方法が欲しいところです。

問題その2の対策: 非公開/公開リソースのエンドポイントを分割する

KitchHikeでは、この問題に対してそもそもエンドポイントを分けるというアプローチを取りました。ポイントは以下2点です。

  • クエリパラメータでの条件分岐ではなく、コントローラ自体を分ける
  • ログインユーザーのリソースは self のネームスペース配下に一元化

いうなれば、if文による分岐をコントローラのエンドポイントの分岐に置き換えたものです。

コードで示すと以下のようになります。

# /kitchens/:id
class KitchensController < ApplicationController
  def show
    # 全てのユーザー向けのレスポンス
  end
end

# /self/kitchens/:id
class Self::KitchensController < ApplicationController
  def show
    # ログインしているユーザー向けのレスポンス
  end
end

エンドポイント分割のメリット

このアプローチを取ることによって以下のようなメリットがありました。

  1. 条件分岐ミスのリスクを根本からなくせた
  2. エンドポイント分割によるスコープの明確化

この問題は、バグによる影響がセキュリティ上の問題に繋がる、非常に大きいものです。その点、このアプローチはそのリスクを根本から取り除けるという点で、非常に大きなメリットがあるものでした。

また、エンドポイントを分割したことで、スコープの切り分けがはっきりしました。そのことにより、開発者の間でもデータの公開範囲についての些細な認識違いや漏れが減り、コミュニケーションが非常に取りやすくなるというメリットもありました。

エンドポイント分割のデメリット

一方、このアプローチには以下のような気をつけるべき点もあります。

  1. APIコール数が増える
  2. 分割したコントローラでのコードの重複が起こりやすい

まず、エンドポイントが増えた関係上、APIをコールする回数は当然増えてしまいます。今後パフォーマンス上の問題が発生するのであれば、エンドポイント分割の原則を崩すのも必要になるかもしれません。ただ、現在のところ、N+1のコールにはならないので許容しています。

また、基本的に同じリソースへのアクセスとなるため、コードの重複が発生しやすくなります。この問題に対しては、モデル層にコードを寄せて、極力コントローラのコードを薄くすることで対応しています。

まとめ

以上、API開発において問題になりがちな「意図せぬレスポンス」問題について、KitchHikeでのアプローチをご紹介しました。整理すると以下のような感じでしょうか

  • API開発にあたっては以下のような2つの意図しないレスポンスの発生パターンがある
    1. そもそも公開してはいけないレスポンス項目の公開ミス
    2. 特定のユーザー向けのレスポンス項目の公開範囲設定ミス
  • 1.の課題に対しては、ホワイトリスト方式の指定が効果的
  • 2.の課題に対しては、エンドポイントを分割することで発生リスクをなくすことができる

正直なところAPI開発は試行錯誤の連続だったのですが、もし同じような課題にぶつかっていらっしゃる開発者の方にとって、今回ご紹介したKitchHikeでの事例が少しでも参考になれば幸いです。

Sketch 初心者は入れておきたいオススメのプラグイン6選

f:id:featherplain:20170607100302p:plain

こんにちは。デザイナーの羽野です。KitchHike では Web プロダクトのデザインとコーディングを担当しています。最近は React Native に手を出し始めました。Web の知識が活かせるのでなかなか楽しいです。

さて、デザインの際は Sketch を使っているのですが、恐らく知らない方はもういないんじゃないでしょうか。Sketch の便利さを語ろうとしたらそれだけでひとつ記事が書けてしまいますが、便利さのひとつにプラグインを活用した拡張性の高さが挙げられます。

というわけで、今回は Sketch を使うにあたってぜひとも入れておきたいプラグインを厳選して紹介したいと思います。必須系プラグインに的を絞ってあるので、Sketch 初心者の方はぜひ入れてみてくださいね。

Runner

http://sketchrunner.com/

Sketch はデフォルトでも便利ですが、使っていくうちにドロップダウンメニューで操作するのがだんだん億劫になってきますよね。プラグインが増えたり、階層が深くなればなるほどめんどくさいです。

そんな課題を一気に解決してくれるのが Runner というプラグインです。一言で説明すると Sketch 版 Alfred といったところでしょうか。Run + <プラグイン名> または <アクション> で プラグインを検索できたり、Sketch のメニューアクションを実行できます。もちろんプラグインのアクションもすべて Runner 上で一発です。私はもう Runner がないと仕事ができない体になってしまいました。

f:id:featherplain:20170606215028p:plain

プラグインの管理ツールとしても使える

アップデート

f:id:featherplain:20170606220149p:plain Cmd + ( 日本語キーボードの場合は Cmd + Shift + ) で Runner を立ち上げると、アップデートできるプラグインがある場合は図のように <数字>UPDATES と表示されます。これをクリックするとアップデート可能なプラグインが表示されるので、 Update All をクリックすればあとはよしなにやってくれます。

f:id:featherplain:20170606220152p:plain

インストール

プラグインの検索やインストールには install + を実行します。キーワードを入力すれば勝手にサジェストしてくれるので非常に便利です。

f:id:featherplain:20170606215118p:plain

シンボルの検索や挿入が快適

insert + でシンボルの検索と挿入ができます。シンボルのプレビューが表示されるのがありがたいですね。さらにレイヤー ( シンボル ) を選択した状態で alt + Enter すると、シンボルの置き換えができるので、いちいちドロップダウンをたどる必要もありません。

f:id:featherplain:20170606220416p:plain

Craft

https://www.invisionapp.com/craft

Craft Official from InVision on Vimeo.

言わずと知れた Craft は有名すぎて説明する必要もないかもしれません。もともとは Silver Flows という名前だったのですが、inVision に買収されて Craft として生まれ変わった経緯があります。

Craft は inVision と連携することでその真価を発揮するのですが、できることは以下です。

  • Sync: Sketch のアートボードと inVision を同期する
  • Library: アセットの共有機能
  • Data: JSON や Web 上の画像データなどを挿入できる
  • Duplicate: 要素の複製 ( Sketch デフォルトの Make Grid と同じような機能 )
  • Prototype: Sketch 上でプロトタイピングができる
  • Freehand: 文字通り、フリーハンドでメモを残せる

Rename It

https://github.com/rodi01/RenameIt

f:id:featherplain:20170606220535p:plain

Sketch で作業しているとアートボード名やレイヤー名がだんだんとカオスになってきますが、 Rename it を使えばレイヤー名の整理も一発です。GitHub の README に使い方の gif 動画がはってあるので、ぜひそちらを見て欲しいのですが、できることを簡単にまとめておくと、以下のことができます。

  • 昇順と降順の連番 ( 例: item_%n )
  • 元の名前を保持したまま新たに名前をつける ( 例: item という名前に新たに連番をつける *_%n )
  • レイヤーやアートボードの Width と Height を付与

User Flows

https://abynim.github.io/UserFlows/

f:id:featherplain:20170606220746p:plain

User Flows は Sketch 上で画面遷移図を作成できるプラグインです。if / else のような条件分岐つきのフローも作成できます。

inVision や Prott のようなプロトタイピングツールを使用していれば普段は必要ないかもしれませんが、資料として画面遷移図を提出することがある場合は重宝するでしょう。

Sketch Measure

http://utom.design/measure/

f:id:featherplain:20170606220912p:plain

Sketch Measure は Sketch 上にオブジェクトのサイズやマージンなどの情報を記載できるプラグインです。ワンクリックで簡単にデザインの指示書を作成できます。Ctrl + Shift + Bでツールバーを呼び出して使います。

Symbol Organizer

http://sketchapp.rocks/plugins/symbol-organizer/

Sketch のシンボルページに生成されたシンボルを整理できるプラグインです。Sketch のシンボル機能はとても強力ですが、プラグインを入れない状態でシンボルを作ると、ひたすら横一列に作成順に集約されてしまいます。

Before

こちらがプラグインを使わない状態です。整理しようにもちょっと収拾がつかない感じですね。

f:id:featherplain:20170606221325p:plain

After

Symbol Organizer を使ってシンボルを整理しました。Lv1/Icons/Icon といった半角スラッシュ / を活用した階層が反映された状態できれいに並び替えられました。

f:id:featherplain:20170606221309p:plain

f:id:featherplain:20170606221317p:plain

いかがでしたでしょうか。Sketch を使いこなしている人には知っているものばかりかもしれませんが、もし「知らなかった!」というものがひとつでもあれば、ぜひインストールして試してみてくださいね。プラグインを活用して素敵な Sketch ライフを送りましょう!

DHH流のルーティングで得られるメリットと、取り入れる上でのポイント

はじめに

こんにちは。KitchHikeエンジニアの小川です。KitchHikeでは主にサーバーサイドを担当しています。

少し前のものですが、「DHHはどのようにRailsのコントローラを書くのか (原文)」というすばらしい記事があります。Railsのコントローラ分割の(DHH流)ベストプラクティスについて解説した記事なのですが、私はこの記事に大変感銘を受け、KitchHikeのルーティング定義にもこのプラクティスを取り入れるようになりました。

本日はこのDHH流ルーティングを取り入れることで得られるメリット、実際の routes.rb でのルーティング定義のしかたについて紹介したいと思います。

DHH流ルーティングとは?何がうれしいの?

詳しくは元記事を是非とも読んで下さい・・・なのですが、かいつまむと、ここで示されているのはたったひとつの単純明快なルールです。

コントローラはデフォルトのCRUDアクションindex、show、new、edit、create、update、destroyのみを使うべきだということです。その他のアクションはどれも専用の(それ自体はデフォルトのCRUDアクションしか持たない)コントローラの作成につながるのです。 DHHはどのようにRailsのコントローラを書くのか

このルーティングを取り入れるとどのようなメリットがあるのでしょうか?私は大きく2点あると考えています。

1.コントローラのスリム化

重要な責務のコントローラはえてしてアクションが増えがちです。気づけばコントローラの行数が増え、肥大化を招いてしまいます。

コントローラをスリム化するRailsのベストプラクティスとしては、古からよく聞かれる「Skinny Controller, Fat Model」というものがあります。ただ、これはモデル層にビジネスロジックを寄せようというアプローチで、アクション数自体が増えてしまった場合にはあまりマッチするものではありません。

それに対し、DHH流のルーティングのアプローチは、細かくコントローラを分割するというものです。Railsのデフォルトのアクション( indexshowneweditcreateupdatedestroy )のみに制限することで、むやみにコントローラ内にアクションが増えることを防いでくれます。結果として、コントローラのスリム化につながります。

KitchHike内でもモデル層に対するアプローチ(たとえばConcernsへの分割など)は継続的に行っていましたが、こうしたコントローラ自体を分割する方法は意識していなかったので非常に新鮮でした。

2. ルーティングのルールが明解になる

コーディングルールは厳格に決まっていても、ルーティングのルールはあまり意識されていないところが実は多いのではないでしょうか?KitchHikeもそのきらいがあり、ルーティング定義は実装者の裁量まかせで、カスタムアクションにも特にルールは設けていませんでした。

カスタムアクション自体は悪いものではありませんが、ルールがないと実装者ごとのばらつきが出がちです。また、仮にルールを厳密に決めたとしても、それが複雑であれば誰もわからなくなってしまうでしょう。

それに対し、DHH流のルーティングのいいところはルールが単純で分かりやすいところです。はっきりしたルールがあれば、実装者によるばらつきは出にくくなりますし、更に開発者間でのコミュニケーションが取りやすくなります。

DHH流ルーティングのケーススタディ

さて、次はDHH流ルーティングを実際にどう取り入れるかをご紹介します。コントローラを分割するタイミングは大体以下の2パターンが多いのではないかと思います。

  • リソースにサブアクションを追加する
  • リソースをフィルタリングする

以降はこの2つのパターンをそれぞれ見ていきましょう。

ケーススタディその1. リソースのサブアクション編

ある特定のリソースに対して、CRUD以外の追加のアクションを付け加えたい場合です。 たとえば recipe のリソースに対して、公開/非公開のアクションを付け加えるとしたらどうでしょう?

カスタムアクションを追加するのであれば、コントローラは以下のようになるでしょう。

class RecipesController < ApplicationController
  def index
  end
  # デフォルトのCRUDアクションが続く

  def publish
    # 公開処理
  end

  def unpublish
    # 非公開処理
  end
end

でもDHH流に書くのであれば、ここはコントローラの分割のポイントです。

class Recipes::PublicationsController < ApplicationController
  def update
    # 公開処理
  end

  def destroy
    # 非公開処理
  end
end

recipe リソースの配下に publication という公開/非公開を担うサブアクションを追加し、 update に公開処理を、 destroy に非公開処理をマッピングしてみました。

URLは以下のイメージです。

/recipes/:id/publication

素朴に書いてみる

次はルーティングです。

素朴にルーティング定義を書いてみるとこんな感じになるのではないでしょうか。何だかうまくいきそうです。

resources :recipes do
  resource :publication, only: [:update, :destroy]
end
  recipe_publication PATCH  /recipes/:recipe_id/publication(.:format)  publications#update
                     PUT    /recipes/:recipe_id/publication(.:format)  publications#update
                     DELETE /recipes/:recipe_id/publication(.:format)  publications#destroy
             recipes GET    /recipes(.:format)                         recipes#index
                     POST   /recipes(.:format)                         recipes#create
          new_recipe GET    /recipes/new(.:format)                     recipes#new
         edit_recipe GET    /recipes/:id/edit(.:format)                recipes#edit
              recipe GET    /recipes/:id(.:format)                     recipes#show
                     PATCH  /recipes/:id(.:format)                     recipes#update
                     PUT    /recipes/:id(.:format)                     recipes#update
                     DELETE /recipes/:id(.:format)                     recipes#destroy

ダメでした。コントローラーがネストされず、 publication そのままになってしまっています。

Railsはデフォルトではネストしたリソースに対して、ネームスペースを使ってくれないのです。

namespace を使ってみる

では namespace を使ってみるとどうでしょうか。

namespace :recipes do
  resource :publication, only: [:update, :destroy]
end
resources :recipes

ルーティングを見てみると・・・。

  recipes_publication PATCH  /recipes/publication(.:format)            recipes/publications#update
                      PUT    /recipes/publication(.:format)            recipes/publications#update
                      DELETE /recipes/publication(.:format)            recipes/publications#destroy
              recipes GET    /recipes(.:format)                        recipes#index
                      POST   /recipes(.:format)                        recipes#create
           new_recipe GET    /recipes/new(.:format)                    recipes#new
          edit_recipe GET    /recipes/:id/edit(.:format)               recipes#edit
               recipe GET    /recipes/:id(.:format)                    recipes#show
                      PATCH  /recipes/:id(.:format)                    recipes#update
                      PUT    /recipes/:id(.:format)                    recipes#update
                      DELETE /recipes/:id(.:format)                    recipes#destroy

こちらも上手くいきません。 publication のアクションを行うリソースのidがどこかへ行ってしまいました。 namespace だからよく考えると当たり前ですよね・・・。

module を使おう

先の記事のコメント にもありますが、今回のケースだと module を使うのがよいです。以下のように書いてみましょう。

resources :recipes do
  resource :publication, only: [:update, :destroy], module: "recipes"
end
  recipe_publication PATCH  /recipes/:recipe_id/publication(.:format)  recipes/publications#update
                     PUT    /recipes/:recipe_id/publication(.:format)  recipes/publications#update
                     DELETE /recipes/:recipe_id/publication(.:format)  recipes/publications#destroy
             recipes GET    /recipes(.:format)                         recipes#index
                     POST   /recipes(.:format)                         recipes#create
          new_recipe GET    /recipes/new(.:format)                     recipes#new
         edit_recipe GET    /recipes/:id/edit(.:format)                recipes#edit
              recipe GET    /recipes/:id(.:format)                     recipes#show
                     PATCH  /recipes/:id(.:format)                     recipes#update
                     PUT    /recipes/:id(.:format)                     recipes#update
                     DELETE /recipes/:id(.:format)                     recipes#destroy

理想通りのルーティングになりました!

module はコントローラーの名前空間を変更したいときに使います。今回はリソース名と同じ名前を module に記述して、対応するコントローラー名をネストさせています。

ケーススタディその2. リソースのフィルタリング編

次はリソースを特定の条件でフィルタリングするケースを考えてみたいと思います。recipe を「下書き」という条件でフィルタリングしてみましょう。

コントローラは先ほどと同じく分割して書いてみました。

class Recipes::DraftsController < ApplicationController
  def index
    # 下書き一覧
  end
end

素朴に書いてみる

早速ルーティング定義を書きましょう。こんな感じでどうでしょうか。

resources :recipes do
  resources :drafts
end
      recipe_drafts GET    /recipes/:recipe_id/drafts(.:format)           drafts#index
                    POST   /recipes/:recipe_id/drafts(.:format)           drafts#create
   new_recipe_draft GET    /recipes/:recipe_id/drafts/new(.:format)       drafts#new
  edit_recipe_draft GET    /recipes/:recipe_id/drafts/:id/edit(.:format)  drafts#edit
       recipe_draft GET    /recipes/:recipe_id/drafts/:id(.:format)       drafts#show
                    PATCH  /recipes/:recipe_id/drafts/:id(.:format)       drafts#update
                    PUT    /recipes/:recipe_id/drafts/:id(.:format)       drafts#update
                    DELETE /recipes/:recipe_id/drafts/:id(.:format)       drafts#destroy
            recipes GET    /recipes(.:format)                             recipes#index
                    POST   /recipes(.:format)                             recipes#create
         new_recipe GET    /recipes/new(.:format)                         recipes#new
        edit_recipe GET    /recipes/:id/edit(.:format)                    recipes#edit
             recipe GET    /recipes/:id(.:format)                         recipes#show
                    PATCH  /recipes/:id(.:format)                         recipes#update
                    PUT    /recipes/:id(.:format)                         recipes#update
                    DELETE /recipes/:id(.:format)                         recipes#destroy

・・・リソースがネストしたルーティングになってしまいました。今回はサブリソースを表現したいわけではないので、これではダメです。

namespace を使う

また namespace を使ってみましょう。

namespace :recipes do
  resources :drafts
end
resources :recipes
      recipes_drafts GET    /recipes/drafts(.:format)                     recipes/drafts#index
                     POST   /recipes/drafts(.:format)                     recipes/drafts#create
   new_recipes_draft GET    /recipes/drafts/new(.:format)                 recipes/drafts#new
  edit_recipes_draft GET    /recipes/drafts/:id/edit(.:format)            recipes/drafts#edit
       recipes_draft GET    /recipes/drafts/:id(.:format)                 recipes/drafts#show
                     PATCH  /recipes/drafts/:id(.:format)                 recipes/drafts#update
                     PUT    /recipes/drafts/:id(.:format)                 recipes/drafts#update
                     DELETE /recipes/drafts/:id(.:format)                 recipes/drafts#destroy
             recipes GET    /recipes(.:format)                            recipes#index
                     POST   /recipes(.:format)                            recipes#create
          new_recipe GET    /recipes/new(.:format)                        recipes#new
         edit_recipe GET    /recipes/:id/edit(.:format)                   recipes#edit
              recipe GET    /recipes/:id(.:format)                        recipes#show
                     PATCH  /recipes/:id(.:format)                        recipes#update
                     PUT    /recipes/:id(.:format)                        recipes#update
                     DELETE /recipes/:id(.:format)                        recipes#destroy

思ったとおりのルーティングになりました!

が、 routes.rb がいけてないですね・・・。 recipes を書く場所が分散してしまっています。できればまとめたいところです。

さらに、上記では namespace ~resources :recipes の下に書くと、 drafts:id と見なされてルーティングエラーとなります。書き順の依存があるのは何だかもやもやします。

そもそもリソース名を変えてみる

ここで提案したいのがそもそも「リソース名を変えてみる」ということです。下書きではない、通常の recipe リソースを items で表してみるとどうでしょうか。

namespace :recipes do
  resources :items
  resources :drafts
end
       recipes_items GET    /recipes/items(.:format)                      recipes/items#index
                     POST   /recipes/items(.:format)                      recipes/items#create
    new_recipes_item GET    /recipes/items/new(.:format)                  recipes/items#new
   edit_recipes_item GET    /recipes/items/:id/edit(.:format)             recipes/items#edit
        recipes_item GET    /recipes/items/:id(.:format)                  recipes/items#show
                     PATCH  /recipes/items/:id(.:format)                  recipes/items#update
                     PUT    /recipes/items/:id(.:format)                  recipes/items#update
                     DELETE /recipes/items/:id(.:format)                  recipes/items#destroy
      recipes_drafts GET    /recipes/drafts(.:format)                     recipes/drafts#index
                     POST   /recipes/drafts(.:format)                     recipes/drafts#create
   new_recipes_draft GET    /recipes/drafts/new(.:format)                 recipes/drafts#new
  edit_recipes_draft GET    /recipes/drafts/:id/edit(.:format)            recipes/drafts#edit
       recipes_draft GET    /recipes/drafts/:id(.:format)                 recipes/drafts#show
                     PATCH  /recipes/drafts/:id(.:format)                 recipes/drafts#update
                     PUT    /recipes/drafts/:id(.:format)                 recipes/drafts#update
                     DELETE /recipes/drafts/:id(.:format)                 recipes/drafts#destroy

routes.rb も実際のルーティングもすっきりしたのではないでしょうか?

とはいえ、URLも変わってしまいますし、少々やり過ぎな気もします。先ほどの namespace 案との違いは routes.rb を綺麗に書けるかどうか、というあくまでコードだけの問題です。

フィルタリングが複雑な場合などはいいかもしれませんが、単純なものであれば namespace 案で充分だと思います。

まとめ

以上DHH流のルーティングのメリットと実際のルーティング定義についてまとめてみました。あらためて整理するとこんな感じでしょうか。

  • DHH流のルーティングは以下2つのメリットがある。
    • 多数のアクションで肥大化したコントローラのスリム化
    • ルーティングのルールが明確なのでコミュニケーションが取りやすい
  • リソースのサブアクションを表すには module を使うと便利。
  • リソースのフィルタリングを表すには namespace を使ってサブリソースを表現する方法、そもそもリソース名を変更する方法がある。

コミュニケーションが取りやすくなることが個人的にはとても重要に思います。コミュニケーションが取りやすくなることで、メンバー同士でレビューがしやすくなり、ルーティングの改善の機会に繋がりやすいからです。

DHH流ルーティングを取り入れて、一度チームメンバーの皆さんとわいわい話し合ってみてください。きっと楽しいですよ!

KitchHikeでは一緒にルーティングを考えてくれるRailsエンジニアを絶賛募集中です!

www.wantedly.com

KitchHike Tech Blog – First Post

Welcome to the KitchHike Tech Blog!

初めてブログを書いた日を覚えていますか? 個人ブログの記事を遡ってみると、ちょうど10年前の2007年ころのようだ。

2007年は、初代iPhoneがMacworld Expo 2007で発表された年で、LINEはもちろんFacebookの日本語版もまだない。
(Wikipediaによると、LINE 2011年6月公開, Facebook 日本語対応 2008年公開)

10年で世界は変わった。さらに10年後は、世界はどうなっているだろう?

未来を予測するのは難しい。ただ一つ、間違えなく言えることは、10年後も僕らは何かを食べているということだ。食はそれほど特別な普遍性を持っている。

このブログでは、KitchHikeのミッション「“食”でつながる幸せな暮らしを実現する」ためのプロダクトを開発しているメンバーが、デザイン・技術的な視点から記事を書いていきます。

2017年 元旦