RedwoodJS を Ruby on Rails と比較してみる
RedwoodJS
RedwoodJS は JavaScript/TypeScript で構築されたフルスタック Web アプリケーションフレームワークです。RedwoodJS プロジェクト自体は Tom Preston-Werner 氏 (GitHub 創設者であり Gravatar や Jekyll などの作成者) が中心となり始まりました。
私自身もつい最近になって同じ職場の @sakitoさんに存在を教えてもらったばかりです。
RedwoodJS は、READMEから抜粋するだけでも、次のような機能を持ちます。
- フォーマット・ディレクトリ・ビルドなどに関するデフォルト構成
- 単一ファイルによるルーティング定義
- GraphQL Client / API (with Serverless deploy) の構築
- ページ・レイアウトなどのジェネレータ
- CRUD 操作に特化した Scaffold
- バリデーションなどエラー処理も含むフォーム
- ホットリロード
- DB マイグレーション
- GraphQL Directive によるアクセス検証やデータ変換
- ロギング
- Webhook と、送信・受信時の署名の検証および署名
- ビルドによるページのプリレンダリング
- Storybook の統合
- テストコード
- Serverless/ Serverful 両方へのデプロイ
これでも全てではなく、References を見ると a11y や Authentication といった、他にも多くの機能を提供してくれます。
技術的には、一般的なフロントエンド開発で良く利用されるものを組み合わせつつ、それだけでは対応しきれない部分を RedwoodJS 自体が機能提供することで解決しているようです。
Ruby on Rails からの影響
RedwoodJS は Ruby on Rails に影響を受けている部分があります。
たとえば、RedwoodJS には RedwoodRecord と呼ばれる ORM を持ちますが、これは Rails の ActiveRecord に影響を受けた形で設計されており、その旨は RedwoodRecord のリファレンスにも記述があります。
RedwoodRecord is heavily inspired by ActiveRecord which ships with Ruby on Rails. It presents a natural interface to the underlying data in your database, without worry about the particulars of SQL syntax.
他にも、Scaffold を用いてモデルやそれに対応する CRUD 操作とルーティングを一括作成できたり、単数系/複数形を意識した命名規則など、さまざまな箇所で Rails の面影を感じます。
「Ruby on Rails を置き換えるのか?」という疑問
フロントエンドで新しいリッチなフレームワークが登場するとよく耳にするのが 「これは Ruby on Rails を置き換えるのか?」 といった言葉です。
これは私個人の意見ですが、単純なライブラリであれば話は別かもしれませんが、ある程度の規模のフルスタックフレームワークともなると、実際にはプロダクトの性質や組織規模、チームメンバーのスキルセットなどに左右されるため、一概に何か特定のフレームワークが別のフレームワークを置き換えるものではないと考えています。
ただ、プロダクトや組織のフェーズが変わったりメンバーのスキルの変遷に応じて、別のフレームワークへシフトしていくことはあり得ますし、選択肢として知っておく分には損はないかなと思います。
Rails と RedwoodJS の機能を比較して考えてみる
というわけで、Rails で代表される一部機能と RedwoodJS を比較してみることで、RedwoodJS は Rails が実現してきたことをどの程度カバーしてくれるのか、そして、新たに RedwoodJS ではなにが実現できるのかを考えてみます。
ディレクトリ構造
Rails ではディレクトリ構造がある程度定められており、沿っている範囲であればファイルの配備に迷わずに済みます。たとえば、コントローラであれば app/controllers
配下ですし、モデルであれば app/models
に配備されます。
RedwoodJS でも同様で、ある程度は初期化した時点で生成されます。
以下は初期化直後のディレクトリ状態です
├── api
│ ├── db
│ ├── src
│ │ ├── directives
│ │ │ ├── requireAuth
│ │ │ └── skipAuth
│ │ ├── functions
│ │ ├── graphql
│ │ ├── lib
│ │ └── services
│ └── types
├── scripts
└── web
├── public
└── src
├── components
├── layouts
└── pages
├── FatalErrorPage
└── NotFoundPage
細かいディレクトリの説明は省きますが、ある程度はパッと見ると役割はわかりそうですね。
api/
web/
で、バックエンドとフロントエンドで分離しており、DB 向けの seed ファイルなどが scripts/
配下に含まれていくようです。
Rails と決定的に違いそうなのは、ファイルを配備したディレクトリに応じて暗黙的に動作する部分が少ない点かもしれません。(Cell
と呼ばれる機能など、ファイル名に一部依存する機能は存在します)
基本的には JavaScript/TypeScript なので、ファイル間はモジュール解決しないと動作しません。ルーティングも Next.js や Remix のようなファイルシステムベースのルーティングではなく、ルーティング定義を別途記述していく必要があります。
ただ、暗黙的な定義が少ない分、ある程度はディレクトリに自由なカスタマイズ性を持たせることができるのと、依存が宣言的になり、かつ TypeScript での静的検査の恩恵を受けやすいメリットはありそうです。
ORM
個人的に Rails に初めて触れた時に一番感動したのは ORM である ActiveRecord でした。複雑な定義を記述することなく、かつ可読性に優れる API で DB を操作することができます。
先にも少し触れましたが、RedwoodJS では、ActiveRecord に影響を受けた RedwoodRecord と呼ばれる ORM を提供しています。
DB 操作に関してコアとなる部分は同じ ORM である Prisma を利用していますが、RedwoodRecord はそれをラップする形で、より ActiveRecord に近い体験が実現できるさまざまな API を提供しています。
ドキュメントからいくつか抜粋してご紹介します。
// 全件取得
await User.all();
// 新しいレコードの作成
const newUser = await User.create({ name: "FooBar", email: "bar@example.com" });
// 更新
newUser.name = "FooBar";
await newUser.save();
// 保存時のエラー有無の判定
newUser.hasError();
// エラー内容の取得
newUser.errors;
// Primary Key を利用した SELECT
await User.find(1);
// 条件を指定した検索
await User.findBy({ name: "FooBar", email: "bar@example.com" });
// 削除
await newUser.destroy();
// ビルド(保存を伴わないメモリ上でのモデル作成)
const user = User.build({ firstName: "Foo", lastName: "Bar" });
ほかにも、たとえば user.posts.all()
のようなリレーションを辿った取得や、先頭1件だけ取得する first
メソッドなども存在します。
ActiveRecord にある程度触れた方であれば「これはまるで ActiveRecord じゃん!」と思うかもしれません。私もそう思いました。
Prisma 単体に触れた時でも「なんだか ActiveRecord っぽいな〜」と思いましたが、RedwoodRecord に関しては、それを遥かに超えるレベルで ActiveRecord に開発体験を近づけようとしているのを感じます。
マイグレーション
ActiveRecord の強力な機能の一つが DB マイグレーション機能かと思います。DB の継続的な変更をコードとして記録していくことでスキーマを管理します。
RedwoodJS では Prisma Migrateの仕組みを利用してマイグレーションを実現しています。
Prisma Migrate は *.prisma
の変更を基準にマイグレーションファイルを生成します。
Rails 的な視点では、ridgepole と migrate のハイブリッドのような感じでしょうか?
フォーム
Rails では ActiveView のフォームヘルパーによって、モデルと結びつきの強いフォーム要素を生成でき、バリデーションエラー時のエラー表示なども簡単に実現できるようになっています。
RedwoodJS では、React Hook Form のラッパーコンポーネントを用意して対処されています。
たとえば、次のようなフォームコンポーネントが提供されます。
<Form>
<FormError>
<Label>
<InputField>
<SelectField>
<TextAreaField>
<FieldError>
<Submit>
これで全てではなく、ほかにも <CheckboxField>
など必要なフォーム要素は一通り用意されています。
利用例を見てみます。
<Label
name="name"
className="label"
errorClassName="label error"
/>
<TextField
name="name"
className="input"
errorClassName="input error"
validation={{ required: true }}
/>
<FieldError
name="name"
className="error-message"
/>
React Hook Form 風の入力バリデーションルールに加えて、name
に応じて連動してクラス付与やエラー表示を自動的に切り替えてくれます。
Rails とは少し違うようにも感じますが、バックエンドと完全に分離する前提を考えるとモデルに依存した構造にするのは無理がありそうです。React Hook Form をベースにした実装で、プラス α の部分をサポートしてくれる形であるのは妥当な気もします。
ルーティング
Rails では config/routes.rb
にルーティング設定を独自の DSL で記述していきます。
RedwoodJS では、独自のルーターを利用し、単一の設定ファイル(デフォルトでは web/src/Routes.tsx
) にルーティングを記述していきます。
const Routes = () => {
return (
<Router>
<Set wrap={UserLayout}>
<Route path="/user/new" page={NewUserPage} name="newUser" />
<Route path="/user/{id:Int}/edit" page={UserPage} name="editUser" />
<Route path="/user/{id:Int}" page={UserPage} name="user" />
</Set>
<Route notfound page={NotFoundPage} />
</Router>
);
};
export default Routes;
パラメータの簡易的な数値変換などは :Int
指定などで自動的に行なってくれます。
また、ルーティング設定から named route functions を利用することで、型定義の効いた形でパスを生成できます。これは Rails のルーティングヘルパーに近いものだと思われます。
routes.editUser({ id: 123 });
Scaffold
Rails では Scaffold 機能を使うことで、モデルとそれを利用した CRUD 構成の コントローラ・ビューなどを一気に作成できます。
本格運用しているプロダクトではあまり使わないかもしれませんが、開発初期の初期段階やチュートリアルなどではお世話になることもあります。
RedwoodJS でも同様に Scaffold 機能を持っています。
少し異なるのは、あらかじめモデルだけは自力で作成しておく必要がある点です。全体的にスキーマ変更に伴う schema.prisma
の編集は自力で行う必要がある模様です。
yarn redwood generate scaffold User
などと実行すると、 Prisma で定義している User
モデルに基づいて、CRUD 構成のコードをバックエンド・フロントエンド双方にテストコード込みで出力してくれます。
テスト
Rails では標準で Minitest や SystemTest が利用可能で、すぐにさまざまな粒度でのテストコードを書き始めることができます。
RedwoodJS でもかなり強力にテストが統合されており、Jest / testing-library / MSW / Storybook を利用したテストを初期インストール直後から利用できます。
様々なレイヤーにコードが分離されていると、依存コードのモックをどうするかが課題になることもありますが、ドキュメント内を見るだけでも
- Mocking GraphQL Calls
- Mocking Auth
- Cell Mocks
などの説明があり、モック用のヘルパーなども提供されています。
一方で、どうやら 1.5.0 現在では E2E テストについてはサポートされていないようでした。そちらについては Cypress や Playwright など、何らかの方法で独自で検討する必要がありそうです。(見落としだったらすいません)
型
型がどの程度効くのかという点が気になりますが、
Redwood comes with full TypeScript support, and you don't have to give up any of the conveniences that Redwood offers to enjoy all the benefits of a type-safe codebase.
との記述があり、基本的には TypeScript はフルサポートしているようです。
一部 GraphQL やルーティング周りなどの型定義については、型定義生成用コマンドが用意されており、
yarn redwood generate types
と実行すると良い感じに型が効くようになります。
Prisma によるスキーマからの型定義に加え、GraphQL によるバックエンドとフロントエンド間の整合性も取りやすくなっているのは嬉しい点です。
ほかにもたくさん...
あまりにも機能が多すぎて紹介しきれないのでこのあたりにしておきます。
基本的には公式リファレンスを参考に紹介しているだけですので、興味の湧いた方は直接見てみるとよいかもしれません。
所感
個人として過去に数年間 Ruby on Rails を利用しての開発を経験しましたが、Rails の思想は素晴らしいと思っています。レールに乗ることでスピード感のある開発体験を提供してくれるのも実感できましたし、「手間だなぁ」と感じる部分の多くをちょうど良い具合にラップしており、開発で本来実現したい価値を作ることに集中しやすい印象があります。
一方で、ある程度の複雑な UI/UX を構築しようと思うと、モダンフロントエンドとの相性でつらい部分が出てくるのも事実です。 特に、TypeScript での開発体験を知ってしまうと型のない開発はつらく感じてきます。 Ruby でも 3.x 以降では RBS・TypeProf といった静的型検査が利用可能ですが、それでもやはり Rails で完全に型の恩恵を受けた開発ができるのはもう少し未来になりそうです。(期待)
RedwoodJS からは Rails が持っていた思想を受け継ぎつつ、モダンフロントエンドの恩恵を全力で受けられる世界観を感じました。
ただ、Rails ではレールに乗っている間は安全で速いものの、レールから外れるには覚悟が必要な印象があり、その部分も RedwoodJS は受け継いでいるようにも見えました。少し複雑なロジックが必要になったケースなどでは困ることもありそうです。
ちなみに RedwoodJS は、Redwood Startup Fund と呼ばれる、RedwoodJS をメインに採用するスタートアップ企業向けのファンドもやってます。相当本気を感じますね。
まだまだ事例は少ないですが、これからが楽しみなフレームワークですね!
(ちゃんと何か作ってみたいところですね...)
Discussion