Next.jsのApp RouterとRSC、Server Functionsはコロケーションのために使うとちょうどいい
はじめに
こんにちは、PKSHA Technology で EM をしている木村です。
私は PKSHA グループのトライアンフと協業し、面接官のトレーニング(ロールプレイ)のためのプロダクト「面トレAI powered by PKSHA AI Coach(以下、面トレAI)」の開発をおよそ 1 年前から担当しています。
(EM ですがメインの実装者です!)
この面トレAI では Next.js App Router を採用し、 React Server Component(RSC)、Server Functions といった機能を活用しています。
最近大きな脆弱性が見つかり話題となったこれらの技術スタックですが、何が嬉しいのか。いつ使うべきなのか。API Route とどう使い分けるのか。こういった疑問も多く見受けられます。
今回は、面トレAI の開発を進める中で実感した恩恵の 1 つである「コロケーション」を軸にその嬉しさをお話ししていきたいと思っています。
以下この記事で登場するコード例やファイル構成は Next.js v15, Typescript v5, React v19 を想定しています。
コロケーションとは
コロケーション(Colocation)とは、関連するコードを物理的に(ディレクトリ構造上の)近い場所に配置することです。あるいはその程度や設計思想を指すこともあります。
例えば、あるページに関連するコンポーネント、スタイル、テスト、データ取得ロジックなどを、離れたディレクトリにバラバラに置くのではなく、極力同じディレクトリ内にまとめて配置します。例を見てみましょう。
コロケーションが低い例。
app/
├── user/
│ └── page.tsx
├── components/
│ └── user-profile.tsx
├── styles/
│ └── user-profile.css
└── api/
└── user
└── route.ts
コロケーションが高い例。
app/
├── user/
│ ├── page.tsx
│ ├── user-profile.tsx
│ └── user-profile.css
└── api/
└── user
└── route.ts
シンプルな例ですが、コロケーションが言わんとすることは理解できるでしょう。
コロケーションが下がっていく現実の道のり
さて、上の例をみて極端にシンプルな例だと感じた方もいるでしょう。あるいは Tailwind CSS や CSS in JS を利用するような場合、 CSS ファイルは個別に存在しないことになりますから、やや恣意的とも言えます。(実際の開発では私たちも Tailwind CSS を利用していました)
コロケーションが高いことによる嬉しさは、これではあまり実感しにくいでしょう。
ではもう少し現実的に、例えばデザインシステムが存在し、components として共通の部品を使っているとすると、例えば次のようになるでしょう。
app/
├── components/ # デザインシステム(共通部品)
│ ├── atom/
│ │ ├── button.tsx
│ │ └── typography.tsx
│ └── template/
│ ├── input.tsx
│ └── card.tsx
├── user/
│ ├── page.tsx
│ └── user-profile.tsx
└── api/
└── user
└── route.ts
さらに現実のコードでは API のテストとフロントエンドのテストや e2e テストを書きたくなるでしょう。
app/
├── components/
│ ├── atom/
│ │ ├── button.tsx
│ │ └── typography.tsx
│ └── template/
│ ├── input.tsx
│ └── card.tsx
├── user/
│ ├── page.tsx
│ └── user-profile.tsx
└── api/
└── user/
└── route.ts
__tests__/ # テスト用のディレクトリ
├── e2e/
│ └── user.scenario.test.ts
├── frontend/
│ └── user-profile.test.tsx
└── api/
└── user.test.ts
さらに普通、user ページは表示するだけとは限りません。新規ユーザーを作成するページ、既存ユーザーを編集するページなど複数存在することは良くあります。
app/
├── components/
│ ├── atom/
│ │ ├── button.tsx
│ │ └── typography.tsx
│ └── template/
│ ├── input.tsx
│ └── card.tsx
├── user/
│ ├── page.tsx # ユーザー一覧
│ ├── user-list.tsx
│ ├── [id]/
│ │ ├── page.tsx # ユーザー詳細
│ │ ├── user-profile.tsx
│ │ └── edit/
│ │ └── page.tsx # 編集
│ └── new/
│ ├── page.tsx # 新規作成
│ └── user-form.tsx
└── api/
└── user/
├── route.ts # GET, POST /api/user
└── [id]/
└── route.ts # GET, PUT, DELETE /api/user/[id]
__tests__/
├── e2e/
│ └── user.scenario.test.ts # シナリオ内部で変更されたテスト
├── frontend/
│ ├── user-list.test.tsx
│ ├── user-profile.test.tsx # 増えたテスト
│ └── user-form.test.tsx # 増えたテスト
└── api/
├── user.test.ts
└── user.[id].test.ts # 増えたテスト
だんだん長くなってきましたが、このように自然とコードが増えていくにつれ、コロケーションが下がっていくことがわかります。
特にコロケーションが下がっている箇所は以下の通りです。
-
user/配下ページから呼ばれるであろうapi/userの距離が遠い -
user/配下ページから呼ばれるであろうcomponents/templateの距離が遠い -
__tests__/から実行される機能の実装されているコードがそれぞれ遠い
一方で、DRY 原則を考えると複数回使いまわされるデザインなら components/template や components/atom のような位置に存在するのは一定の妥当性があると言えます。
また、e2e テストのようなアプリケーション全体を対象とするものであればトップレベルに置くことも自然と言えるでしょう。
コロケーションを上げよう
そこで、そういった事情も勘案しつつコロケーションを上げるために再配置すると例えば以下のようになります。
app/
├── components/
│ └── atom/ # 全体で共有されるプリミティブなコンポーネントたち
│ ├── button.tsx
│ └── typography.tsx
├── user/
│ ├── page.tsx
│ ├── user-list.tsx
│ ├── user-list.test.tsx # テストを実装の近くに
│ ├── _components/ # user機能専用のコンポーネントたち
│ │ ├── input.tsx
│ │ └── card.tsx
│ ├── [id]/
│ │ ├── page.tsx
│ │ ├── user-profile.tsx
│ │ ├── user-profile.test.tsx # テストを実装の近くに
│ │ └── edit/
│ │ └── page.tsx
│ └── new/
│ ├── page.tsx
│ ├── user-form.tsx
│ └── user-form.test.tsx # テストを実装の近くに
└── api/
└── user/
├── route.ts # GET, POST /api/user
├── route.test.ts # テストを実装の近くに
└── [id]/
├── route.ts # GET, PUT, DELETE /api/user/[id]
└── route.test.ts # テストを実装の近くに
__tests__/
└── e2e/
└── user.scenario.test.ts # アプリ全体を対象とするe2eテスト
ここでもう一度コロケーションを下げていた箇所を見直してみましょう。
-
user/配下ページから呼ばれるであろうapi/userの距離が遠い- そのまま
-
user/配下ページから呼ばれるであろうcomponents/templateの距離が遠い- 少し良くなった
-
__tests__/から実行される機能の実装されているコードがそれぞれ遠い- 割と良くなった
2.に関しては異論もあるでしょうが、ページごとの微修正が必要な部分と、デザインシステムのような範囲で決まっているものを分離しつつ、直接的な距離が適切に近づいたものと考えます。
3.に関しては、テストを可能な限り実装の近くに置くことでかなり解消できました。
1.はどうでしょうか。解決できていません。
RSC + Server Functionsを使うことでAPIのコロケーションを上げよう
ここを解決する方法がこの記事の本題である、RSC と Server Functions との組み合わせです。
今の所 API で必要な挙動はいわゆる CRUD です。したがって、GET を RSC に渡し、POST, PUT, DELETE を Server Functions に渡すことで、API の実装を各ページの近くに置くことができます。
Server Functions を action.ts, page.tsx を RSC にしたとすると次のような構成になります。
app/
├── components/
│ └── atom/
│ ├── button.tsx
│ └── typography.tsx
├── user/
│ ├── page.tsx # ユーザー一覧 (RSCとしてサーバーサイドでデータフェッチ)
│ ├── fetch.test.ts # テストを実装の近くに
│ ├── user-list.tsx
│ ├── user-list.test.tsx
│ ├── _components/
│ │ ├── input.tsx
│ │ └── card.tsx
│ ├── [id]/
│ │ ├── page.tsx # ユーザー詳細 (RSCとしてサーバーサイドでデータフェッチ)
│ │ ├── fetch.test.ts # テストを実装の近くに
│ │ ├── action.ts # Server Functions: PUT, DELETE
│ │ ├── action.test.ts # テストを実装の近くに
│ │ ├── user-profile.tsx
│ │ ├── user-profile.test.tsx
│ │ └── edit/
│ │ ├── page.tsx # 編集 (RSCとしてサーバーサイドでデータフェッチ)
│ │ ├── fetch.test.ts # テストを実装の近くに
│ │ ├── action.ts # Server Functions: PUT, DELETE
│ │ └── action.test.ts # テストを実装の近くに
│ └── new/
│ ├── page.tsx # 新規作成(データフェッチが不要な場合が多い)
│ ├── action.ts # Server Functions: POST
│ ├── action.test.ts # テストを実装の近くに
│ ├── user-form.tsx
│ └── user-form.test.tsx
__tests__/
└── e2e/
└── user.scenario.test.ts
こうして API の実装が API Route に存在することで下がっていたコロケーションを、RSC と Server Functions で高めることができました。
データフェッチ関数を page.tsx に書き切るべきかはコード量や使っている DB・データストア、あるいはそういったレイヤーの抽象化のスタイルに依りますが、この記事では踏み込まないものとします。
実践して得られたメリット
App Router 環境においては、RSC と Server Functions を使ってコロケーションを上げることができることはわかりました。
ではその恩恵をどういった形で受けられるでしょうか。ここでは面トレAI の開発において感じられたメリットをご紹介します。
コンフリクトが起きにくい
上の例では、user/ とそれに関連するページが存在するだけでしたが、実際のアプリケーションではもっと多くのページが存在します。
例えば面トレAI は面接官のトレーニング(ロールプレイ)を行うアプリケーションです。「トレーニングを実施する」「トレーニングを設計する」「トレーニング結果を確認する」など多数のページが存在します。
従来の API Route の利用では、それぞれのページからおそらく GET /api/training が発行されるわけですが、それぞれのページに必要な情報が必ずしも一致しているとは限りません。
したがってトレーニング結果だけを修正しようとした時に、別のページへの影響を考えたり、同時に開発していた場合は思わぬコンフリクトが発生したりといった場合が考えられます。
一方で前述の例のように RSC + Server Functions を使っておけば、各ページの機能を充足することに集中できます。DRY(Don't Repeat Yourself)原則には反しますが、この場合は機能的凝縮を実現できる方がメリットは大きいでしょう。
Coding Agentとの相性
また、昨今の Coding Agent を前提とした開発スタイルにおいても、コロケーションの高さは良い影響を与えるというのが実際に開発をしている感想です。
Claude Code や Codex CLI のような LLM をベースとした Coding Agent では、協調する必要のあるコンテクストが小さいほど全体として上手く働いてくれます。これは経験則と原理の両面から正しいと言えます。
プロンプトとしても、以下の 2 つの指示を比較してみましょう。
「app/user/edit 配下にユーザー編集画面を実装して。RSC と Server Functions を使って他の機能とは独立して実装すること。ファイル名や関数のスタイルは app/user 配下を参照して」
「app/user/edit 配下にユーザー編集画面を実装して。API は既存の app/api/user 配下の物を極力使い、必要であれば新たに作って。ファイル名や関数のスタイルは app/user や app/api/user 配下を参照して」
どちらがより素早く正確に実装してくれそうでしょうか。
実は面トレAI の初期バージョンは API Route に寄せたスタイルで開発されており、後者の指示を与えていたことも実際にあります。もちろん時期によってモデルやエージェントの性能は上がっていますから、定量的な比較ではありませんが、やはり前者のほうがかなり上手く実装してきます。
さらに、直接的に指示からアウトプットの質が良くなるだけでなく、複数の Agent に複数のページを別々に作らせる場合も前述のようにコンフリクトが起こりにくくなります。コロケーションの高いコードベースは Coding Agent との相性も良いと言えるでしょう。
使い分け・注意点
良いことばかり書いてきましたが、API Route の使い所が無くなったわけではありません。RSC や Server Functions があらゆる機能の実装で最善とは限りません。
現実的にできないことや得意でないことも存在します。たとえば、以下のようなケースです。
- 並列なリクエスト: Server Functions は 1 つずつシーケンシャルに実行される。複数の軽量なリクエストを並列に投げたい場合はパフォーマンス低下の原因となる
- ストリーミングレスポンス: Server-Sent Events(SSE)やストリーミング API の実装は RSC と相性が悪い。Server Functions の返り値としても実現できない。例えば ChatGPT のようなタイプライターエフェクトは Client Component から API Route を利用する
- ファイルダウンロード: PDF や CSV などのファイルを動的に生成してダウンロードさせる場合、RSC でも Server Functions でも実装しにくい
目安としてはシンプルな CRUD 機能の範疇を超えた場合やパフォーマンスに違和感を感じたらやはり API Route を検討するというのが私の感じている境界線です。
また、Server Functions はサーバーサイドの状態の mutation のために作られたとされています。 Server Functions でデータフェッチをしそうになった場合も立ち止まって考えた方が良いでしょう。
使い分けだけでなく、注意すべき点として、Server Functions は実際には API エンドポイントとして実現されます。入力値のバリデーションを忘れないことや、返り値がクライアントから見えることをうっかり忘れてしまわないように気をつけましょう。
おわりに
この記事では、Next.js App Router における RSC(React Server Component)と Server Functions を活用してコロケーションを高める方法と、その恩恵について紹介しました。
あらゆる場面で最善というわけではありませんが、適切に使えばその恩恵を受けられる場面も意外と多いのではないでしょうか。
コロケーションの考え方や RSC・Server Functions の使いどころについて、皆さんのご意見やプロジェクトでの工夫などあれば、ぜひコメントで教えてください。
Discussion