初めてのNestJS+Prisma2(+Nuxt.js)でサービスをリリースしてみて

今回は個人開発の話です。

諸事情あって趣味(兼ボランティア)で開発している簡単なグループウェアがあるのですが、ここ二か月半くらいのすき間の時間を使って、そのシステムの大規模改修をしていました。

旧システムはNode.jsにGraphQLもどきをオレオレ実装したバックエンドと、plainなVue.jsとBoostrap4を組み合わせた構成です。よく調べもせず「こんな仕組みで作ったら汎用性が効いていいんじゃないか!」と勢いで作ってみたものの、無駄に複雑なつくりになってしまい、機能追加や仕様変更もしにくい状態でした。

(ちなみにその前はSpringBootがバックエンドだったこともあれば、ただのCGIがバックエンドだったこともあればと歴史は長かったりします。)

去年の秋ごろに参加した勉強会で、NestJSとかPrismaとかいうフレームワークがあるぞ!という話をきき、調べてみるとよさげだったので採用することにしました。

選定理由

そんなにじっくり比較検討したというわけでもないんですが、いけそうと思った理由は次のようなところです

  • NestJS
    • 以前にも使っていたSpringBootと使用感が似ていて学習コストが低そうだった
      • デコレーターを使ったAOPな記述スタイルは既視感がありとっつきやすかった
      • DIコンテナが組み込まれていることでロジックを組み立てる部分に集中しやすそうなのがいい
    • バックエンドに高速なFastifyとやらを使ったりおなじみのExpressを使ったり選択できるところ
      • Requestを表すクラスの構造がExpressと違ったりで途中でハマってFastify使うのはやめましたが、余裕が出たらもう一度チャレンジしてみたいです
    • (普段はRailsを書いている中では)Controllerの実装とルーティングの対応が一目でわかり、さらにControllerの中をざっとみればどのServiceに処理を投げているかも一覧で見やすいところ
      • 「このRoutesだからこの名前のControllerを作らなきゃいけない」みたいな縛りがないのもいいですね
      • 最終的にControllerは3つに分割しましたが、コンパクトなAPIを作るだけなら一つのほうがむしろよさそう
  • Prisma
    • 第一にはJS製のORMを使ったことがなかったのでとりあえず使ってみたかったというのが大きいです
    • マイグレーション機能
      • スキーマ定義からマイグレーションファイルを自動生成する仕組みと、それをDBに反映する仕組みが組み込まれていて、楽そうだった
      • 以前SpringBootで作ったときはマイグレーションの管理をPhinxというPHPのライブラリでやっていましたが(長期インターンでお世話になってた会社がそうだったため)そういう面倒くささがなかったという意味で
    • TypeScript対応
      • スキーマ定義から裏側でTS用の型定義を自動生成してくれるため、CRUDの際に変なデータや検索条件が極力混じらないようになっているのが気に入りました

ちなみにフロントエンドはNuxt.js(SPA)+BootstrapVue+axiosの組み合わせにしました。

Nuxt.jsを採用したのは、自分でVueRouterとか入れてSPAを構築するよりもでかいレールに乗っかったほうが楽そうだなと思ったためで、特に深い理由はないです。

(ちなみにSPAにしているのは、もう何年もそういう構成だからというのと、APIを介した疎結合なつくりが管理しやすいと感じているためです)

BoostrapVueは、改修前のバージョンがBootstrap4をベースにしたコンポーネントでできていて、その実装を極力使いまわしたかったためです。前回はBootstrap4を入れていたといってもjQueryを入れたくなかったのでモーダルなど自力で実装していた部分もありますが、今回はBoostrapVueのおかげで便利なModalやらOverlayやら使えて楽でした。

リリースしてみての感想

いろいろとありましたが、先月末の日曜日に無事に新バージョンをリリースすることができました。6回目のメジャーアップデートです。

ちなみにアプリケーションの管理にはPM2を使いました。これは改修前からそうなので引き続きという感じです。 pm2-logrotate と組み合わせると便利です。

  • 良かったところ
    • 「選定理由」に書いてあることの恩恵は一通り受けられたかなと思います
      • 特に検索条件の指定でfindUniqueメソッドを使ったときは、ユニーク制約のない条件を指定した時点でコンパイルエラーになり、よくできているなと思いました
      • スキーマファイルで完成形を定義するだけで差分のマイグレーションが勝手に作成されるのも新感覚でよかったです。ふつうに便利でした
    • NestJSのInterceptor的なものや例外ハンドリングも、ドキュメントがよく整備されていて、セッション管理やエラー処理などスムーズに実装できました
      • 個人開発ではエラー処理にそこまで労力割けないよみたいなところもありましたが割かなくて良いのがいいですね
  • つらかったところ
    • 一番つらかったのは(採用時に気づかなかったのですが)Prismaで悲観的ロックがサポートされていないところです
      • 公式ドキュメントには「楽観的ロックでうまく作ろうよ」みたいなことが書いてあるのですが(&なんとかできたにはできたのですが)そんな大人数で使うサービスでもないしロックしちゃえば楽なのにみたいな気持ちはないでもないです
      • SELECT .. FOR UPDATE は頑張れば使えるということはわかりましたがこれでいいのかなあとか
      • トランザクションも一応はサポートされていますが、いろいろこねくり回してなんとか実装できる最低限の機能という感じです
    • 選定理由のところにも書きましたが、Interceptor的なもの(NestMiddleware)を実装するときにRequestのクラスを拡張するのですが、それがExpressとFastifyとで違い、(ドキュメントや参考になる実装が少なく)うまく作れませんでした
      • バックエンドを色々切り替えられるといっても、そういうのは使っている特定の実装に依存するんだなと
    • Prismaが生成するマイグレーションファイルのテーブル名がWindowsだと全部小文字になるのにLinuxではそれだと動かない問題
      • これはどちらかというとMySQLの設定の問題ですが、開発環境がWindows10で、MySQLはWindows上ではデフォルトで全部小文字でテーブル名を保存するので、その状態で自動生成されたマイグレーションファイルは小文字になるようです
      • Linux上ではデフォルトで大文字小文字を区別する設定になっているそうで。。今回は本番サーバの設定をWindowsの場合と同じにすることで解決しました
    • Prismaのrejectを判定するのがちょっと面倒問題
      • (自分がドキュメントを十分に読み込んでいないだけなのかもしれないですが)例えばrejectされたときに、理由(「レコードが見つからない」とか)を判断するコードがきれいに書きにくいなと。どんなオブジェクトか調べて、それがPrismaのエラーならエラーコードを見る、みたいな
        • Railsだと ActiveRecord::NotFound のインスタンスかどうか調べればいいんですが
    • DTOにビジネスロジックを実装できない問題
      • まあData Transferが目的のオブジェクトなのでビジネスロジック書けないのは仕方ないですが、じゃあどこに書くんだというのは考えどころでした。結局Service層で使うためのEntity的なクラスを定義したところもあります
      • Prismaが勝手にテーブル名に対応する型定義(userならUserとか)を作ってくれていますが、その定義名が自分で作ったEntityのクラス名(User)と被って面倒くさいなあとかもありました
    • unique条件じゃない検索で結果が0件だったときに落ちるメソッドがほしい
      • Railsでいう find_by! 的なメソッドがあると便利だなと。
      • SpringBootを使っていたときはKotlinと組み合わせていたこともあり ! さえつければ落とすこともできたのですが、TypeScriptは今のところそういう機能はないみたいなので少し冗長な書き方になってしまいました
    • 本番リリース時になぜかDBアクセス時々失敗する問題
      • 本番サーバとほぼ同じテストサーバを構築して、無事にE2Eテスト一通り終わり、いざリリースするぞ!というときに、なぜか本番環境でだけDBアクセスが数十回に一回失敗する事件が起きました。 Can't reach database server at 127.0.0.1:3306 とかなんとか
      • 色々調べてよくわからず、同時接続数変えたりIPv4とv6切り替えたりIP指定じゃなくてlocalhostにしてみたりいろいろ試しましたが、どれも解決に至らず
      • 以前読んだ記事でSELinuxが有効だと何かが起きるようなことが書いてあったので試しに無効化してみると見事に収まったので、何かの設定が厳しすぎたのだろうという結論にいたりました
        • それでいいのか!という感じはあるのですが、クローズド(アクセス制限含め)なサービスなので今回はいったん切ったままでもよいでしょうということに
        • リリースから一週間経ちましたが今のところ再現せず。。

「つらかったこと」がだいぶ長くなってしまいましたが、それでも全体的な開発はサクサク進みましたし(NestJSの開発モードの変更監視&反映は一瞬でした)、Prismaのおかげで動かす前からエラーに気づけた部分も多々あったため、快適に開発できた方だと思います。トランザクションの設計も含めいろいろと考えたり学んだりできたのもよかったです。

あとは(いろいろ議論はありつつも)Railsがよくできたフレームワークだということは改めて実感しました。

インフラ構築の部分について、最後に少しモヤモヤする部分もありましたが、それは今後の課題ということにしておきます。この際サーバレスな何かにデプロイするでもいいかもしれません。(根本解決なのかという突っ込みはなしで

またしばらく運用&追加開発してみて何かあれば書いてみようと思います。

PAGE TOP