TypeScriptでWebサービス開発(Apollo Server/Prisma/Next.js etc...)
最近開発しているBtoB SaaSサービスの技術スタックを、RailsからNode.jsに移行した。
これにより、フロントエンドもバックエンドも全てをTypeScriptで統一することができた。
特にNode.jsのWebバックエンドの構成について、まだまだ世の中に知見が少ない気がしているので記事にしておく。
Webバックエンド - Node.js(TypeScript)
Nexus/Apollo Server (Webサーバー)
GraphQLサーバーとして、Apollo ServerのコードファーストなアプローチでのラッパーであるNexusを使っている。
Railsからの移行を決断できたのも、Apollo ServerとPrismaにより、外部との通信が型付きで、かつ開発体験よく書けるようになたから、というのが大きくある。
数年前の段階だと、素のexpressを使ってWebサーバーを立てるようなsimpleすぎる方法や、TypeORMのようなデコレーターベースでのORMが主流のように見え、個人的にこれらは厳しかった。
GraphQLがもたらす恩恵は様々ありつつも、特にメリットを感じているのは、GraphQLのスキーマを書くことにより、リクエストとレスポンスがバリデーションされて手軽に型がつけられることだった。
Apolloは最近大型の資金調達もしており、フロントエンドも含めGraphQLを使った構成がデファクトになっていく未来が来て欲しいと感じる。
基本的に大満足だが、細かい悩みは以下。
- 複雑なオブジェクト構造をGraphQLのスキーマに書き下すのが面倒。ドメインレイヤーなどで一度型を書いていると、もう一度型を書き直すのが無駄に感じる
- 複雑すぎる型については、開発の初期はJSON型(=any)に逃して、フロントでレスポンスに
as
を使ってアサーションしている。as
を使いたくないのは勿論だが、必要に応じてこういったことができるのも、フロントエンドとバックエンドを同一言語で統一しているメリットだと思う。
- 複雑すぎる型については、開発の初期はJSON型(=any)に逃して、フロントでレスポンスに
- Nexusは記法が多少気持ち悪いが慣れる。とはいえ、型エラーが分かりづらかったりするので、素直にスキーマファーストなアプローチでも良かったかも
Apollo Federation
ちなみに、Rails(GraphQLサーバー)からNode.js(Apollo ServerによるGraphQLサーバー)への移行はApollo Federationを使って段階的に行った。
Apollo Federationは、簡単に言えば「複数のGraphQLサーバーを束ねて1つのGraphQLサーバーになってくれるBFF」である。
これを使って、Railsのエンドポイント(正確には、Railsで定義しているGraphQLのQuery/Mutationのフィールド)を徐々にNode.jsのエンドポイントに移行していき、Railsへのリクエストが0になった時点でRailsを停止するという移行作業を行った。
クライアント側にもサーバー側にもほぼ追加作業は不要で、federatinサーバーを立てるだけで型に守られつつBFFができるのでとても便利だった。
Prisma (ORM)
ORMにはprismaを使っている。
まだまだ開発中という雰囲気はありつつ、次世代ORMと名乗るにふさわしい体験を提供しており、これからのデファクトになりそうだと感じている。
DBレイヤから型安全に開発できるのはとても気持ち良い。
デメリット
- Active Recordと比べると記述量がかなり多くなるし似たようなコードが増える。
- Active Recordに慣れた身としてはかなりげんなりするポイントではありつつ、とはいえ書き続けていたら一定慣れた。型の恩恵を得られるメリットと、記述量が増えるデメリットを比べて、(多少嫌々だが)メリットを重視して良いかなと思う。
- トランザクションのサポート、migrationのサポートなど所々が貧弱
bee-queue (非同期ジョブ)
Node.jsの非同期ジョブとして一番スターが多かったのは bullで、その次期バージョンである bullmq を当初使っていたが、ドキュメントが貧弱で辛かったのと、stolled jobのハンドリング方法が分からなかったのでbee-queueに乗り換えた。
機能が充実しているというほどではないが、最低限非同期ジョブとして使える程度の機能は揃っている。
Webインターフェース(sidekiq/web的なもの)はArenaがあるが、機能が貧弱だったので自作した。リアルタイムで稼働状況やジョブのログを見れるようにしたので気に入っている。
Node.jsの非同期ジョブについては初見で使いづらいライブラリが多く、選定にやや苦労したので、Active JobやSidekiqのような枯れていてかつEasyな方法を提供しているRuby界隈はやはり偉大だな…と感じた。
その他
ディレクトリ構成とProject References
以下のようにモジュラーモノリス、レイヤードアーキテクチャ風にディレクトリを作っている。
src
├── moduleA
│ ├── common
│ ├── infra
│ │ ├── prisma
│ │ ├── ...
│ ├── domain
│ │ ├── common
│ │ ├── service
│ │ ├── repository
│ ├── presentation
│ │ ├── common
│ │ ├── web
│ │ ├── cli
│ │ ├── worker
├── moduleB
│ ├── ...
※これが「正しい」レイヤードアーキテクチャなのか特に自信がないが、チームで共通認識がとれており、事業ドメインに適しており、そして開発を一番スムーズに回せるアーキテクチャが一番良いアーキテクチャだと考えているので、「正しい」アーキテクチャを実現したいという気持ちはあまりない。
※とはいえ色々な人の意見を聞きたいので、感想は歓迎です
各モジュール、モジュール以下の第1層などはProject Referenceを使って依存関係が厳密に定義されており、例えばdomainからpresentationを呼びだすなどはできなくなっている。
こうやってレイヤー間の依存関係を明示できるのもTypeScriptのメリットだと思う。
参考
Ajv
外界とのやりとりは基本的にApollo ServerとPrismaが担い、このレイヤで型がついてくれる。
しかし、自前で外部と通信したり、RDBMSのJSONカラムを使ったりするとany型がソースコードに紛れ込んでしまうので、型のバリデーションが必要になる。
今回は一番有名そうだったバリデーションライブラリのAjvを使った。
JSON Schemaを使ってバリデーションをしてくれるのだが、型を書いてJSON Schemaも書くのが二度手間で非常にダルいのと、型とJSON Schemaが一致しなかったときのエラーが分かりづらいのと、特定のパターンだと型チェックが非常に遅くなるなどいろいろ辛く、他のライブラリを検討している。
linter(eslint, prettier)
eslintの設定は尊敬するHERP社の@ryota-ka先生が公開してくれているconfigをベースに使っている。
かなり厳しいルールだが、特にtypescript-eslintのおかげで型安全な開発が強制されて良い。
jest
テストライブラリはjest、httpのモックにnockを使っている。
この辺は慣れればrspec書くのとそこまで大差ない感想。しかしテスト結果がややみづらいかな。
Webフロントエンド - React.js
Next.js
元々はVue.jsが大好きでNuxt.jsでの開発をしていたが、Node.js移行の少し前にNext.jsに乗り換えた。
3年ほど前にはNext.jsはまだまだ…みたいな雰囲気が漂っていたと記憶しているが、Vercelの頑張りにより、今や不自由もなくリッチな開発体験を提供してくれるフレームワークとなったように思う。
Vueとの差分としては、Vue/Nuxt.jsの方がやはりeasyな解決策を多数用意してくれておりとっつきやすいが、jsxによる型安全な開発がしやすいのが一番大きな差分という認識。
(とはいえ、最近はVueでも型安全に開発できるのかな?最近あまり追えていない)
Apollo Client
Apollo ClientとGraphQL Code Generatorを使ってサーバーとの通信を行っている。
これにより、通信のリクエスト/レスポンスに型がつくのがとても良い。
また、キャッシュの機構も非常に優秀なので、もはやフロントエンド開発でApollo Clientを(そしてGraphQLを)使わない手はないくらいに思っている。
(例えば、あるレコードの中身を更新した際に、別のページで表示していたレコードの内容が勝手に書き換わってくれる、みたいなのが非常に楽。)
Ant Design, TailwindCSS
UIライブラリにはAnt Design、細かいスタイル調整にはTailwind CSS、さらに細かい調整にはインラインのstyle記法を使っている。グローバルに影響するstyleを使うことはほぼない。
Ant Designは豊富で実践的なUIコンポーネントをeasyな形で提供しているのが気に入っている。
例えばテーブルコンポーネントについて、これだけの機能を提供してくれるUIライブラリを他に見たことがない。
難点としては、Ant Designのテーマ変更はLessによるカスタマイズしか用意されていない点。Lessは最近界隈から見放されている感じがあり、Next.jsもlessのサポートを終了しているので、今後が不安ではある。
ディレクトリ構成
フロントエンドもバックエンドと同じように、レイヤードアーキテクチャ風のディレクトリ構成にしている。
src
├── common
├── infra
│ ├── apollo_client
│ ├── ...
├── domain
│ ├── common
│ ├── repository
│ │ ├── graphql
│ ├── app_service
│ ├── service
├── presentation
│ ├── common
│ ├── hooks
│ ├── components
│ ├── pages
こちらも厳密にレイヤードアーキテクチャにしたかったというよりは、ディレクトリ構成に統一感を出したかったのと、
jsxによる表示を司る層(presentation
)と、jsxを書かない表示のためのロジックの層(app_service
)、ドメインロジック(service
)を分けて考えた方が整理しやすく、またテストも書きやすかったという理由でこうしている。
infra
とか repository
とかは多少無理がある名前な気はしつつ、バックエンドと統一できて満足している。
(ディレクトリ構成自体は満足しているが、ディレクトリ名が適切かどうかはかなり自信がないので、誰か教えて欲しい)
インフラ - Render
インフラにはRenderというherokuライクなPaaSを使っている。
複雑なインフラの必要がないWebサービス運用には本当におすすめなので、ぜひ使ってみて欲しい。
参考
その他使用中のサービス
もはやTypeScript関係ないけど晒しておく。
Posthog
イベントトラッキングツール。Mixpanel+Hotjarのようなもの。
これひとつで各種の行動分析からセッション録画までできて非常に高機能。プラグインも多数あり、データのエクスポートも容易。
OSS版を使えばトラッキングデータに対して直接SQLを書くこともできるので、なんでも分析できる。
プロダクトの改善のための分析はもちろん、エラー通知が来た時に、ユーザーがどんな行動をしてエラーが出たのかなどをクイックに確認できたりして良い。
Renderだとワンクリックでデプロイできて便利。
Bugsnag
エラー通知のために利用。Sentryの違いとかはあまり詳しくないが、特に不満はない。
Intercom
お問い合わせ対応、また機能のリリース通知などで利用している。
Bugsnagでエラー監視・Posthogでエラーの状況の詳細を確認・Intercomでお問い合わせ対応といったコンボが強い。(ここまでやることはあまりないが)
NPSサーベイなども気軽に実施できるので、スタートアップの強い味方。
Redash
BIツール。OSS版をRenderで運用している。
Renderだとワンクリックでデプロイできて(略)
Discussion