【Next.js】Server Actions時代のアーキテクチャ入門
この記事は?
アーキテクチャというとエンジニアによって定義が曖昧になりがちです。本来、システム構成やインフラ構成まで含めてアーキテクチャという意見も尤もですが、この記事ではNext.js(App router以降)のアーキテクチャ(フロントエンド、バックエンド)について考察していきます。フロント、バックエンドを分ける理由は、バックエンドにとってはデータベースを含むモデリングが重要になる一方、フロントエンドにとってはデザインシステムを含む整理が重要になってくるからです。
アーキテクチャの基礎知識
(※ ここは復習部分になるので、適宜読み飛ばしてください。
初めにアーキテクチャの基礎について触れるのは、基礎がわかっていれば応用が効きやすいため(敢えてServer Actionsにしていますが、それ以外の技術構成でも応用可能)です。
フロントエンドの実装でデザインシステムが重要なのは、効率良く作業を進めるためにデザイナーとフロントエンドエンジニアが共同で作業を進める際の共通認識になるからです。フロントエンドでは後述で示す共通層を作ることで、デザイントークンを意識した実装を行うようにします。一方で、バックエンドはドメインとビジネスロジックを含むため、それぞれに対して効率よくテストを書く必要があります。そこで本記事では、バックエンドのアーキテクチャとしてレポジトリパターンを紹介し、DIで永続化層は切り離すようにします。これは、プロジェクトをよりテスタブルにしてスケーラビリティを増すためです。一方、シード期の開発など他は捨てスピードを最重視するようなレポジトリであれば単なるMVCなどより軽量なものを選択することも手段になってきます。
フロントエンドのデザインパターン
実際のアーキテクトの業務では様々なものを知っておき選ぶことが大事になってくるので、全て紹介しますが、著者としておすすめなのは初めのパターン1です。(理由は後述。)
パターン1: Store project files outside of app
この構成では、appフォルダには最低限の各ページファイルしか含めない構成を取ります。app routerでは、app配下にあるファイルをpage.xxx命名をトリガーにルーティングとして使用することから、appフォルダは本来ルーティングのためにあるフォルダと言えます。つまり、この構成をとることによって、appフォルダをルーティングに特化した層として分離することが可能です。
your-project
|- components
|- lib
|- app
|- dashboard
|- page.tsx
|- page.tsx
パターン2: Store project files in top-level folders inside of app
こちらでは、appの中にcomponentsやlibといったルーティングに直接紐づかないパーツも含め全てを入れているような構成となっています。
your-project
|- app
|- components
|- lib
|- dashboard
|- page.tsx
|- page.tsx
パターン3: Split project files by feature or route
こちらもappの中にcomponentsやlibといったルーティングに直接紐づかないパーツも含め全てを入れているような構成となっていますが、パターン2と違う部分は、それぞれのドメインの中にcomponentsやlibと言ったパーツが含まれていることです。
your-project
|- app
|- components
|- lib
|- dashboard
|- components
|- lib
|- page.tsx
|- page.tsx
結局どうするのがいいのか?
これらを踏まえて、appフォルダにはルーティングに直接からむページを入れつつ、app外にドメインごとにcomponentsを切る以下の構成にすることで、appフォルダをルーティングに特化した層として切り離しつつ、components配下はドメインで区切ることが可能になります。(アプリ全体で使えるような基礎となるデザイントークンはshardなどの別フォルダに入れておきます。)
your-project
|- components
|- shared
|- dashboard
|- header.tsx
|- footer.tsx
|- lib
|- app
|- dashboard
|- page.tsx
|- page.tsx
バックエンドのデザインパターン
バックエンドとして、レポジトリパターンを踏襲したディレクトリ構成を紹介します。なお、DB接続にはPrismaを用いていますが、適宜Knexなど好きなものに置き換えてもらって構いません。
your-project
|- app
|- api
|- action
|- repository
|- service
|- lib
|- test
|- prisma
|- schema.prisma
依存の順番としては、schema(ドメイン層) <- repository(データアクセス層) <- service(ビジネスロジック層) <- action/api(ユースケース層)の順に並びます。実際にユースケース層で処理を呼び出す際には、DIによってビジネスロジックの処理をユースケース層に注入します。そうすることによってビジネスロジックと永続化処理が分離され、テスタブルなディレクトリを実現することができるのです。特に、Next.jsでは継続したアップデートが常にあり、テストを書いて正しくコードが動くことを担保していきたいためテストを容易に行えるような構成を保っておきたいからです。
以上を踏まえて、Userという会員情報をPrismaから取得し、Server ActionまたはAPI routesで情報を使うまでのフローをサンプルとして書き実際の実装はどうなるのか?確かめてみます。
ドメイン層(/prisma/schema.prisma)
Prismaを使うとモデリングはschema.prismaに定義されます。
model User {
id String @id @default(uuid())
name String
email String @unique
password String
}
レポジトリー層(/repository)
いわゆる永続化を行うための層はレポジトリー層に書いていきます。
const createUserRepository = () => {
return {
findUserById: (userId) => {
return // DBから特定のユーザーを取得する処理を返す
}
};
};
ビジネスロジック層(/service)
サービス層ではレポジトリー層で定義した処理を呼び出しますが、レポジトリー層と違うのはこちらには永続化の処理はかかず、あくまでビジネスロジックを書くということです。
const createUserService = (userRepository) => {
return {
getUserById: (userId) => {
return userRepository.findUserById(userId);
}
};
};
ユースケース層(/api, /action)
/api, /actionでは依存性の注入を行い、各ユースケースに対する処理を書いていきます。
// 依存性の注入
const userRepository = createUserRepository();
const userService = createUserService(userRepository);
const user = userService.getUserById(userId);
// 以下にAPI routes及びActionでビジネス要件を満たす処理が続いていく
...
テスト(/test)
テストではテストピラミッドの戦略に従って、Actions及びAPI Routesを構成するそれぞれの処理を中心に、ユニットテストを厚めに書いていくことを意識します。そうすることで、ActionとAPI routeはもちろん、仮にそういった既存のユースケース相当にかわる新規の何かが出てきても、それぞれの処理に対するUTが各々の動作を担保することが可能です。
import * from "repository/user.ts"
import * from "service/user.ts"
// 以下にAPI routes及びActionを構成する処理それぞれに単体テストを書いていく
...
おまけ) インフラはどうすべきか?
Next.jsは新規開発で最良になる選択の一つで、初めはインフラに時間をかけず、AmplifyなどVercelなどにスピーディなデプロイをすることはとても良い考えだと考えています。インフラのカスタマイズが必要になり、AWSなどフルスクラッチの構成に切り替えるとしても、ここにあるような構成をとって、テストを書き、メンテナンス性を保っていればリデプロイが容易です。最悪、長期運用で負債化してしまった場合は、以下のような方法をとって負債の返済を行なっていきます。
リファレンス
Discussion
有益な記事ありがとうございます!!
とんでもないです。
お役に立てたなら何よりです!
App Routerでフォーム処理を行う際にServer Actionを記載すると思いますがそちらは、ディレクトリを切りますか?もしくは、ページ内に直接記載しますか?
もし、切る場合、フロントエンドのどの層に置くかアドバイスをもらえると幸いです。
分けますね!actionだったり別なフォルダを切ってそっちに入れるのがいいと思います!