🐕

Next.js App Router API実装マトリクス

に公開

APIの実装についてマトリクスにしようと思い、疑問点をいろいろ調査してたら、結構な量になったので、せっかくなのでまとめておきました。
ご活用いただければ幸いです。

第1章 パラダイムシフト:サーバーコンポーネントとクライアントコンポーネント

Next.jsのApp Routerは、単なるルーティングシステムのアップデートにとどまらず、Webアプリケーションの構築方法そのものを根本から変革するパラダイムシフトを導入しました。
その核心をなすのが、サーバーコンポーネント(Server Components)とクライアントコンポーネント(Client Components)という2つのコンポーネントモデルです。

このサーバーとクライアントの二分法を理解することは、後続のデータフェッチやミューテーション(データ更新)の各パターンを習得するための絶対的な前提条件となります。

1.1 サーバーファーストのデフォルト:サーバーコンポーネントの役割と力

App Routerの最も根源的な変更点は、appディレクトリ内のすべてのコンポーネントが、デフォルトでReact Server Components(RSC)として扱われることです。

これは、従来のPages Routerがデフォルトでクライアントサイドレンダリング(CSR)のコンポーネントを想定していた点からの大きな転換です。
この設計思想は、パフォーマンスとセキュリティを初期状態から最大化することを目的としています。

サーバーコンポーネントは、その名の通り、サーバー環境でのみレンダリングされ、実行されるコンポーネントです。そのため、クライアントサイドでは実行されるべきでない、あるいはサーバーサイドで実行する方がはるかに効率的な処理に最適です。

主なユースケース

  • データソースへの近接アクセス: データベース、ORM(Object-Relational Mapper)、またはプライベートなAPIからのデータフェッチを、データソースに近いサーバー環境で直接行うことができます。これにより、ロジックとデータが物理的に近接し、不要なネットワーク遅延を削減します。

  • 機密情報の保護: APIキー、認証トークン、環境変数といった機密情報を、クライアントサイドのJavaScriptバンドルに一切含めることなく安全に利用できます。コードがクライアントに送信されないため、漏洩リスクが根本的に排除されます。ただし、サーバーで実行すべきコードを誤ってクライアント側で実行してしまうと、機密情報が漏洩するなどの重大な事故につながる可能性があります。このリスクを防ぐため、server-onlyのようなライブラリを導入し、サーバー専用のコードがクライアントから誤って呼び出されることをビルド時点で確実に防ぐことが重要です。

  • クライアントバンドルの削減: サーバーコンポーネントのコードはクライアントに送信されないため、ブラウザがダウンロード・解析・実行するJavaScriptの量を大幅に削減できます。これは、ウェブパフォーマンスの重要指標であるFirst Contentful Paint (FCP)やTime to Interactive (TTI)を直接的に改善し、アプリケーションの初期ロード時間を劇的に短縮します。

サーバーコンポーネントのレンダリングプロセスは特異です。Next.jsはサーバー上でRSCをレンダリングし、その結果を React Server Component Payload (RSC Payload) と呼ばれる特殊でコンパクトなバイナリ形式に変換します。
クライアントに送信されるのは、コンポーネントのJavaScriptコードそのものではなく、このRSC Payloadです。これにより、クライアントは最小限の仕事でUIを構築できるのです。

1.2 インタラクティビティの境界線:クライアントコンポーネントの定義と利用

サーバーコンポーネントが静的なUIのレンダリングとデータフェッチに特化しているのに対し、現代的なWebアプリケーションに不可欠なインタラクティビティはクライアントコンポーネントが担います。
開発者は、インタラクティブな機能が必要な箇所で、明示的にクライアントコンポーネントを作成する必要があります。

これを実現するのが'use client'ディレクティブです。ファイルの最上部にこのディレクティブを記述することで、そのコンポーネントとその依存関係がクライアントサイドで実行されるべきであることをNext.jsとReactに伝えます。このディレクティブは、サーバーとクライアントのモジュールグラフ間に明確な「境界」を引く役割を果たします。

クライアントサイドの機能

クライアントコンポーネントは、以下のようなブラウザ環境でのみ利用可能な機能に必須です。

  • 状態管理とイベントリスナー: useStateuseReducerによる状態管理、onClickonChangeといったユーザーイベントへの応答。
  • ライフサイクルエフェクト: useEffectuseLayoutEffectを用いた、レンダリング後の副作用の実行(例:DOM操作、サードパーティライブラリの初期化)。
  • ブラウザ専用APIへのアクセス: windowオブジェクト、localStoragenavigator.geolocationなど、サーバー環境には存在しないAPIの利用。
  • カスタムフック: 上記の機能に依存するカスタムフック(例:状態管理ライブラリのフック)の使用。

'use client'ディレクティブでマークされたファイルは、それがインポートする全てのモジュール(他のコンポーネントやライブラリを含む)と共に、クライアントサイドのJavaScriptバンドルの一部と見なされます。これは、この境界線の設定がアプリケーションのパフォーマンスに直接的な影響を与えることを意味します。
インタラクティブな部分を不必要に広げると、クライアントバンドルが肥大化し、サーバーコンポーネントがもたらすパフォーマンス上の利点を損なう可能性があります。

1.3 通信の架け橋:RSC Payloadとコンポジションパターン

サーバーとクライアントという異なる環境で実行されるコンポーネント群が、どのようにして一つのアプリケーションとして統合されるのでしょうか。その鍵を握るのが、前述のRSC Payloadです。

RSC Payloadの詳細

RSC Payloadは、サーバーとクライアント間の通信を司る重要なデータ構造です。これには以下の情報が含まれています。

  • サーバーコンポーネントのレンダリング結果 : サーバーでレンダリングされたUIの、HTMLに似たツリー構造。
  • クライアントコンポーネントのプレースホルダー : クライアントコンポーネントがどこに挿入されるべきかを示す「 空き地 」。
  • クライアントコンポーネントの参照 : プレースホルダーに対応するクライアントコンポーネントのJavaScriptファイルへの参照。
  • シリアライズされたProps : サーバーコンポーネントからクライアントコンポーネントへ渡されるProps。

ハイドレーションプロセス

  1. 初回ロード時: ブラウザはまず、サーバーで事前生成されたHTMLを受け取ります。これにより、非インタラクティブながらも非常に高速なUIのプレビューが表示されます(FCPの向上)。
  2. RSC Payloadの適用: 次に、RSC Payloadがクライアントにストリーミングされます。Reactはこのペイロードを使い、サーバーコンポーネントのレンダリング結果とクライアントコンポーネントのプレースホルダーを統合(Reconcile)します。
  3. インタラクティブ化: 最後に、必要なJavaScriptバンドルがダウンロードされ、クライアントコンポーネントを「ハイドレーション」します。ハイドレーションとは、静的なHTMLにイベントリスナーなどをアタッチし、完全にインタラクティブな状態にするプロセスです。

高度なコンポジション - 「クライアントコンポーネントをツリーの末端に」

このアーキテクチャから導き出される最も重要な設計パターンは、「クライアントコンポーネントを可能な限り小さく、そしてコンポーネントツリーの深い階層(末端、あるいはLeaf)に配置する」というものです。
これを実現する一般的な手法が、サーバーコンポーネントをクライアントコンポーネントの childrenやPropsとして渡すことです。

例えば、静的な<Layout>(サーバーコンポーネント)が、インタラクティブな<SearchBar>(クライアントコンポーネント)と静的な<Logo>(サーバーコンポーネント)を子要素として受け取ることができます。
この場合、<Layout><Logo>はサーバーでレンダリングされ、クライアントバンドルには含まれません。クライアントバンドルには<SearchBar>のコードのみが含まれ、全体のパフォーマンスが最適化されます。
同様に、クライアントサイドの<Dashboard>コンポーネントが、サーバーでレンダリングされた<Profile>コンポーネントをchildrenとして受け取ることも可能です。

このアーキテクチャがもたらす必然的な制約として、サーバーコンポーネントからクライアントコンポーネントに渡されるPropsは、 必ずシリアライズ可能でなければならない という点があります。
関数、Dateオブジェクト、Map、Setといった複雑な非シリアライズ可能なデータ型を直接Propsとして渡すことはできません。これらはRSC Payloadを通じてネットワーク経由で転送できないためです。

このアーキテクチャは、単なる技術的な選択肢ではなく、開発者に特定の設計思想を促すものです。
それは、アプリケーションの各部分がどの環境で実行されるべきかを意識的に判断し、パフォーマンスとインタラクティビティのバランスを最適化するという思想です。
サーバーコンポーネントをデフォルトとし、インタラクティビティが必要な部分のみをクライアントコンポーネントとして切り出すことで、Next.js App Routerは本質的に高性能なアプリケーションの構築を可能にしているのです。

第2章 データ参照(Read):サーバーとクライアントのパターン

CRUD操作の「Read」は、アプリケーションの最も基本的な機能の一つです。App Routerでは、データをどこで、いつ、どのようにフェッチするかが、パフォーマンスとユーザーエクスペリエンスに決定的な影響を与えます。
この章では、初期表示のためにサーバーサイドでデータを取得するパターンと、動的な更新のためにクライアントサイドで取得するパターンを、それぞれの長所と最適なシナリオと共に詳述します。

2.1 サーバーサイドデータフェッチ:推奨されるデフォルト

App Routerの哲学の中心には、可能な限り多くの処理をサーバーサイドで行うという考えがあります。データフェッチも例外ではなく、サーバーコンポーネント内でデータを取得することが、最も基本的かつ推奨されるパターンです。

拡張されたfetch API

Next.jsは、標準のWeb fetch APIを拡張し、サーバーサイドでのデータフェッチに強力なキャッシュと再検証のレイヤーを追加しました。これはサーバーコンポーネントにおけるデータ取得の基盤となる機能です。

リクエストの自動重複排除(Deduplication)

Reactは、同一のレンダリングパス内で同じURLとオプションを持つfetchリクエストを自動的にメモ化(Request Memorization)します。これにより、例えばレイアウトとページの両方で同じデータを必要とする場合でも、実際のネットワークリクエストは一度しか発生しません。これはパフォーマンスの最適化に大きく貢献します。

なお、fetchAPIを使用しないデータアクセス(例:ORMによるデータベースへの直接クエリ)の場合、この自動重複排除は機能しません。その場合は、Reactが提供するcache関数でデータ取得関数をラップすることで、同様の重複排除を実現できます。

キャッシュ戦略

拡張されたfetch APIは、Next.jsのData Cacheをきめ細かく制御するオプションを提供します。

  • cache: 'force-cache': これがデフォルトの動作です。レスポンスを永続的にキャッシュし、ビルド時に一度だけ、あるいは最初のアクセス時にフェッチされたデータが以降のリクエストで再利用されます。静的なコンテンツに最適です。
  • cache: 'no-store': リクエストごとに常に新しいデータをフェッチします。これにより、キャッシュは一切使用されず、ルートは動的レンダリングにオプトインします。常に最新のデータが必要な場合に用います。

再検証(Revalidation)戦略

キャッシュされたデータを更新するための戦略も提供されています。

  • 時間ベースの再検証(Time-based Revalidation): fetch(URL, { next: { revalidate: 3600 } })のように指定します。これはIncremental Static Regeneration (ISR) を実装するもので、指定された期間(この例では3600秒)はキャッシュされたデータが提供されます。期間が過ぎた後の最初のリクエストに対しては、まず古い(stale)データが返され、バックグラウンドでデータの再検証(再フェッチ)が実行されます。再検証が成功すると、キャッシュが新しいデータで更新されます。
  • オンデマンド再検証(On-demand Revalidation): イベント(通常はServer ActionやRoute Handler内での呼び出し)をトリガーとしてキャッシュを無効化します。これにより、データの更新を即座に反映させることができます。
    • revalidatePath('/blog'): 特定のパスに関連するキャッシュをパージします。
    • revalidateTag('posts'): fetchリクエスト時にnext: { tags: ['posts'] }のようにタグ付けされた全てのキャッシュエントリを一度にパージします。複数のページにまたがるデータを効率的に無効化できるため、非常に強力です。

高度なパフォーマンスパターン

  • 並列フェッチと逐次フェッチ(Parallel vs. Sequential Fetching): 兄弟関係にあるコンポーネントや、コンポーネントのトップレベルで開始されるデータフェッチは、デフォルトで並列に実行されます。しかし、あるフェッチが別のフェッチの結果に依存している場合(「ウォーターフォール」)、データ取得は逐次的に行われ、全体のロード時間が長くなります。これが意図的でない場合は、独立したフェッチをPromise.allなどを用いて同時に開始することで、パフォーマンスを改善できます。
  • Preloadパターン: 親子コンポーネント間でのウォーターフォールを避けるため、親コンポーネントが子コンポーネントで必要となるデータを「先行ロード(preload)」するパターンです。親がフェッチを開始し、その後子がレンダリングされて同じフェッチを呼び出すと、Reactのfetchメモ化機能によりリクエストが重複排除され、既存のPromiseが再利用されます。
  • ストリーミング(<Suspense>loading.js: 遅いデータフェッチ中にユーザー体験を向上させるため、Next.jsはUIをストリーミング配信できます。データをフェッチするコンポーネントを<Suspense>でラップすると、データのロード中にフォールバックUI(例:スケルトンローダー)を表示できます。また、loading.jsファイルをルートセグメントに配置すると、そのセグメント全体に対して自動的に<Suspense>境界が作成されます。

2.2 クライアントサイドデータフェッチ:インタラクティブなシナリオ向け

サーバーサイドフェッチが強力である一方、初期ロード後に行われるユーザーインタラクションに応じてデータを取得する必要があるシナリオも数多く存在します。

クライアントでフェッチするタイミング

このパターンは、初期ページロード後に発生するクライアントサイドの状態やユーザーインタラクションに依存してデータが必要になる場合に不可欠です。具体的な例としては、リストの絞り込み検索、無限スクロール、ユーザー専用ダッシュボードの動的更新などが挙げられます。

レガシーなuseEffectパターン

伝統的な方法は、useEffectフックでfetch呼び出しをトリガーし、useStateでデータ、ローディング状態、エラーステートを管理するものです。この方法は手動での状態管理が煩雑で、キャッシュや再検証といった高度な機能が欠けているため、現代のアプリケーション開発では一般的に推奨されません。

モダンな状態管理ライブラリ(SWR & TanStack Query)

SWRやTanStack Query(旧React Query)といったライブラリは、クライアントサイドのデータフェッチにおける推奨アプローチです。これらのライブラリは、以下のような堅牢なソリューションをすぐに利用できる形で提供します。

  • キャッシュ、ウィンドウフォーカス時の再検証、ポーリング、オプティミスティックUI更新など、複雑なデータ同期ロジック。
  • ローディング、エラー、データ状態の管理を簡素化するフックベースのAPI。

SWRは軽量でシンプルさが特徴であり、TanStack Queryは自動ガベージコレクションやオフラインミューテーションサポートなど、より豊富な機能を提供します。

ハイドレーションパターン(両者の長所を活かす)

これは、複雑でインタラクティブなページにおいて最も先進的かつ強力なパターンです。サーバーサイドレンダリングの高速な初期表示と、クライアントサイドライブラリのリッチな機能を両立させます。

  1. サーバー(SC)でのプリフェッチ: サーバーコンポーネント(例:page.tsx)内で、クライアントライブラリが提供するサーバーサイドヘルパー(例:queryClient.prefetchQuery)を使用して、初期データを事前にフェッチします。
  2. サーバー(SC)でのデハイドレート: クエリクライアントのキャッシュを「デハイドレート(dehydrate)」します。これは、プリフェッチしたデータをシリアライズ可能な形式に変換するプロセスです。
  3. Props経由での橋渡し: デハイドレートされた状態を、インタラクティブなUIをラップするクライアントコンポーネントにPropsとして渡します。このラッパーは、<HydrationBoundary>のようなプロバイダーコンポーネントであることが多いです。
  4. クライアント(CC)でのリハイドレート: クライアントサイドで、ライブラリはサーバーから渡されたデハイドレート済みの状態を使って自身のキャッシュを「リハイドレート(rehydrate)」します。これにより、子コンポーネント内のuseQueryuseSWRといったフックは、初回のレンダリング時にキャッシュ内にデータを見つけることができます。結果として、クライアントサイドでの余分なfetchが回避され、UIが即座に表示されます。その後は、ライブラリが後続のクライアントサイドインタラクション(再フェッチ、ミューテーションなど)の責務を引き継ぎます。

この一連の流れは、Next.jsのサーバーサイドの能力とクライアントサイドのライブラリの能力が、単に二者択一の関係にあるのではなく、協調して動作するものであることを示しています。サーバーサイドのfetchは、もはや単なるデータ取得ユーティリティではなく、キャッシュと再検証機能を含んだ、それ自体が完結したサーバーサイドのデータ状態管理システムと見なせます。一方で、クライアントサイドのライブラリの役割は、単なる「データ取得ツール」から、サーバーとクライアント間の複雑な状態を同期させる「状態シンクロナイザー」へと進化しています。したがって、現代のApp Routerにおけるデータフェッチの議論は、「サーバーかクライアントか」という対立構造ではなく、「サーバーとクライアントのフェッチをいかに最適に組み合わせるか」という統合的な視点が求められるのです。

第3章 データ更新(Create, Update, Delete):Server Actions vs. Route Handlers

データの参照(Read)がアプリケーションの静的な側面を担うのに対し、作成(Create)、更新(Update)、削除(Delete)といったミューテーションは、アプリケーションの動的な側面を司ります。App Routerでは、これらのデータ更新処理を実装するために、モダンで統合的な「Server Actions」と、伝統的で分離された「Route Handlers」(API Routes)という2つの主要なアプローチが提供されています。

3.1 統合的アプローチ:Server Actions

Server Actionsは、App Routerにおけるデータミューテーションの主役となる機能です。これは、サーバーサイドで実行されるロジックを、あたかもクライアントサイドの関数であるかのように直接コンポーネントから呼び出せるようにする、画期的な仕組みです。

基本概念

Server Actionsは、'use server'ディレクティブによってマークされた非同期関数であり、サーバー上でのみ実行されますが、クライアントコンポーネントやサーバーコンポーネントから直接呼び出すことが可能です。Next.jsアプリケーション内でのデータ更新処理には、このServer Actionsの使用が強く推奨されます。

定義方法

  • インライン定義(サーバーコンポーネント内): サーバーコンポーネント内のasync関数の本体の先頭に'use server'を記述することで、その関数をServer Actionとして定義できます。
  • ファイル単位での定義(クライアント・サーバー両用): app/actions.tsのような独立したファイルの先頭に'use server'を記述すると、そのファイルからエクスポートされる全ての関数がServer Actionとなります。この方法は、クライアントコンポーネントからServer Actionをインポートして使用するために必須です。

呼び出し方法

  • フォーム(推奨): Server Actionを<form>要素のactionプロパティや、<button type="submit">formActionプロパティに直接渡す方法が最も一般的です。このアプローチの大きな利点は プログレッシブエンハンスメント です。JavaScriptが無効な環境でも、フォームは標準的なHTMLフォームとして機能し、サーバーにデータを送信できます。
  • イベントハンドラ: onClickonSubmitといったイベントハンドラから直接呼び出すことも可能です。この場合、UIのブロッキングを防ぐためにReact.startTransitionでラップすることが一般的です。

高度なフォームハンドリング(クライアントコンポーネント内のフック)

ReactはServer Actionsと連携する強力なフックを提供しており、これらはクライアントコンポーネント内で使用します。

  • useActionState(旧useFormState): フォームの状態を管理するためのフックです。アクションの実行中(pending)状態や、アクションからの返り値(例:バリデーションエラーメッセージ)をコンポーネントのステートとして受け取ることができます。
  • useFormStatus: 親の<form>の実行状態(pendingなど)を提供するフックです。フォーム内のどこからでも呼び出せるため、送信ボタンの無効化やスピナーの表示などを、状態をPropsで引き回すことなく簡単に実装できます。
  • useOptimistic: サーバーアクションが完了する前にUIを楽観的(optimistic)に更新するためのフックです。ユーザーは即座にフィードバックを得られ、もしサーバーでの処理が失敗した場合はUIが自動的に元の状態に戻ります。これにより、非常に高速で滑らかなユーザーエクスペリエンスが実現します。

キャッシュとの統合

Server ActionsはNext.jsのデータキャッシュシステムと深く統合されています。ミューテーションが成功した後、アクション内でrevalidatePath()revalidateTag()を呼び出すことで、関連するデータのキャッシュを無効化し、UIの再レンダリングとデータ再フェッチをトリガーできます。

リダイレクト

ミューテーション成功後にユーザーを別のページに遷移させたい場合は、next/navigationからredirect()関数をインポートし、アクション内で呼び出します。

3.2 分離的アプローチ:Route Handlers (API Routes)

Route Handlersは、appディレクトリ内にroute.ts(または.js)ファイルを配置することで、伝統的なRESTful APIエンドポイントを作成する機能です(例:app/api/posts/route.ts)。これはPages RouterにおけるAPI Routesの直接的な後継機能にあたります。

実装

route.tsファイル内で、GET, POST, PUT, PATCH, DELETEといったHTTPメソッドに対応する名前のasync関数をエクスポートすることで、エンドポイントのロジックを定義します。

呼び出し

これらのエンドポイントは、クライアントコンポーネント内のイベントハンドラなどから、標準のfetch APIを用いて呼び出されます。

ユースケース

Server Actionsが強力であるにもかかわらず、Route Handlersが依然として必要不可欠なシナリオが存在します。

  • 外部へのAPI公開: モバイルアプリケーションやサードパーティのサービス、あるいは自社の別のWebサービスなど、Next.jsアプリケーションの外部クライアントに対してAPIを公開する場合。
  • Webhookの受信: StripeやGitHubといった外部サービスからのWebhookリクエストを受け取るエンドポイントとして機能させる場合。
  • 伝統的なREST APIの構築: 全てのHTTPメソッドを厳密に制御し、RESTの原則に準拠したAPIを構築する必要がある場合。

コラム
ここで以下の疑問が湧くことがあります。

疑問:クライアントコンポーネントから直接バックエンドのAPIにfetchを送信すればシンプルではないか?
結論:Next.jsと外部バックエンドで構成する場合でも、クライアントコンポーネントからRoute Handlerを経由してfetchするユースケースは非常に多く、むしろ推奨されるベストプラクティスです。

直接バックエンドにfetchするのではなく、Route Handlerを「一枚挟む」ことには、主にセキュリティと保守性の観点から明確なメリットがあります。Route Handlerは単なる中継点ではなく、BFF (Backend for Frontend) として重要な役割を担います。

解説

  1. セキュリティの向上:APIキーや認証情報の隠蔽
    これがRoute Handlerを使う最大の理由です。

多くのバックエンドAPIは、認証のためにAPIキーや秘密のトークンをリクエストに含める必要があります。

直接fetchする場合(非推奨)
クライアントコンポーネントから直接fetchすると、そのAPIキーをブラウザに送信するJavaScriptコードに含める必要があります。これは、APIキーがユーザーに丸見えになってしまうことを意味し、極めて危険なセキュリティリスクとなります。

Route Handlerを経由する場合(推奨)
クライアントはAPIキーを持たず、Next.js内のRoute Handlerにリクエストを送ります。Route Handlerはサーバー環境で実行されるため、process.envを通じてサーバーにしか保存されていない安全なAPIキーを使って、バックエンドにリクエストを転送(プロキシ)します。

  1. BFFとしての役割:レスポンスの整形と最適化

バックエンドAPIが返すデータ構造が、必ずしもフロントエンドでそのまま使いやすいとは限りません。

Route Handlerを間に挟むことで、バックエンドからのレスポンスをフロントエンドが最も使いやすい形に加工・整形してから返すことができます。

  • 複数のAPIから取得したデータをマージして返す
  • 不要なデータフィールドを削ってペイロードを軽量化する
  • フィールド名をフロントエンドの命名規則に合わせる
  • 複雑なデータを計算・加工して返す

これにより、クライアントコンポーネント側のロジックがシンプルになり、責務を明確に分離できます。

  1. CORS問題の回避
    ブラウザから外部のドメインにリクエストを送る場合、CORS(Cross-Origin Resource Sharing)ポリシーという壁に直面します。外部バックエンド側で、Next.jsアプリのドメインからのリクエストを許可する設定が必要になります。

Route Handlerを使えば、ブラウザからのリクエストは常に**同一オリジン(自身のNext.jsサーバー)**の/api/...に対して行われるため、CORSの問題を気にする必要がなくなります。(サーバー間の通信にはCORSの制約はありません。)

  1. 外部APIからの抽象化(保守性の向上)
    バックエンドAPIのエンドポイントや仕様が変更された場合を考えてみましょう。

直接fetchする場合
そのAPIを呼び出しているすべてのクライアントコンポーネントを修正する必要があります。

Route Handlerを経由する場合
変更の影響をRoute Handler内で吸収できます。Route Handlerが返すデータ構造さえ変えなければ、フロントエンド側のコードは一切変更する必要がありません。これにより、フロントエンドとバックエンドを疎結合に保ち、保守性を高めることができます。

端的に言うと、これらの恩恵を受けないパターンの場合は、Router Handler を使わなくても直接バックエンドにfetchすればいいということになります。ただそういうパターンは少ないのでRoute Handler を活用する構成が推奨されます。

3.3 戦略的な意思決定:Server Actions vs. Route Handlers

Server ActionsとRoute Handlersのどちらを選択するかは、技術的な優劣ではなく、目的とコンテキストに基づいたアーキテクチャ上の決定です。

RPC vs. REST

この2つのアプローチの根本的な違いは、通信のモデルにあります。Server ActionsはRPCRemote Procedure Call)の一形態です。クライアントは、あたかもローカルの関数を呼び出すかのように、サーバー上の手続き(procedure)を呼び出します。一方、Route Handlersは REST(Representational State Transfer) の原則に基づいてAPIを構築するためのものです。クライアントは、リソース(resource)に対してHTTPメソッドを用いて状態遷移をリクエストします。このRPC対RESTというパラダイムの違いが、それぞれの最適なユースケースを決定づけます。

開発者体験と統合

Next.jsアプリケーション内部からのミューテーションにおいては、Server Actionsが圧倒的に優れた開発者体験(DX)を提供します。コンポーネントとの密な連携、ツール不要のエンドツーエンドの型安全性、ReactフックやNext.jsキャッシュとのシームレスな統合は、開発を大幅に簡素化し、堅牢にします。対照的に、Route Handlersは、エンドポイントの定義、クライアントサイドでの fetch呼び出し、ローディング・エラー・成功状態の自前管理といった、より多くの定型的なコードを必要とします。

セキュリティに関する誤解

Server Actionsが「エンドポイントを公開しないから安全」という見方は一般的な誤解です。実際には、Server Actionsも内部的には一意のIDを持つPOSTエンドポイントを作成しており、適切に保護されなければ外部から呼び出される可能性があります。セキュリティ(認証・認可)は、どちらのアプローチを選択するかにかかわらず、開発者が責任を持って実装しなければならない課題です。Server Actionsの主な価値は、自身のアプリケーションUIから発生するミューテーションを安全かつ効率的に処理することにあります。

結論:補完的な関係

最終的に、Server ActionsとRoute Handlersは競合するものではなく、互いに補完しあう関係にあります。どちらを選択すべきかは、そのAPIエンドポイントの「利用者(オーディエンス)」は誰か、という問いによって明確になります。

  • Server Actionsを使用すべき場合: APIの利用者が自身のNext.jsアプリケーションのUIフォーム、ボタンなど)である場合。
  • Route Handlersを使用すべき場合: APIの利用者がそれ以外の全て外部のモバイルアプリ、サードパーティサービス、Webhookなど)である場合。

この明確な指針は、開発者がアーキテクチャを設計する上での重要な意思決定を助けます。まずAPIの利用者を特定すること。それが、最適なツールを選択するための第一歩となるのです。

第4章 実装マトリクス

これまでの章で詳述してきたNext.js App RouterにおけるAPI通信の多様なパターンを、ユーザーの要求の中心であるマトリクス形式に集約します。この表は、CRUD操作と実行コンテキスト(サーバー/クライアント)を軸に、各シナリオで推奨されるパターン、実装の要約、長所、そして考慮事項を一覧化したものです。
このマトリクスは、複雑で広範にわたるNext.jsのエコシステムから得られる情報を、単一の構造化された比較可能なビューに統合したものであり、開発者が一目で情報を把握し、情報に基づいた迅速な意思決定を下すことを可能にします。これにより、広範なドキュメントやコミュニティの議論を読み解く時間を大幅に節約し、一般的なアーキテクチャ上の誤りを未然に防ぐための実践的なガイドとして機能します。

Next.js App Router API実装マトリクス

Next.js App Router API実装マトリクス

操作 コンテキスト 推奨パターン 実装概要と主要API/フック 長所 短所・考慮事項
参照 (R) サーバーコンポーネント (初期ロード/静的) サーバーサイドfetchとキャッシング asyncサーバーコンポーネント内で const data = await fetch(URL, { cache: 'force-cache' }); を使用 最高のパフォーマンス:最速のFCP、最小のクライアントJS。セキュア:APIキー等の機密情報がサーバーに留まる。シンプル:クライアント側の状態管理が不要 インタラクティブでない初期データに限定される。キャッシュの挙動を明示的に管理する必要がある
参照 (R) サーバーコンポーネント (リクエスト毎に動的) キャッシュなしのサーバーサイドfetch const data = await fetch(URL, { cache: 'no-store' }); を使用。または cookies()headers() のような動的関数を使用 常に最新のデータ:各リクエストで最新のデータを保証。セキュア:APIキー等の機密情報がサーバーに留まる キャッシュされたレスポンスより遅い。データソースが遅い場合、ボトルネックになる可能性がある
参照 (R) クライアントコンポーネント (クライアント主導のインタラクション) Route Handler + SWR/TanStack Query バックエンド: app/api/data/route.tsexport async function GET() {...} を定義。フロントエンド: クライアントコンポーネントで const { data } = useSWR('/api/data', fetcher); を使用 リッチなクライアント状態:キャッシュ、再検証等をライブラリが管理。分離:クライアント/サーバーが明確に分離。複雑なUI(検索、フィルタ等)に適している 定型コードが増える。管理しないとクライアント・サーバー間のウォーターフォールを引き起こす可能性がある。サーバーのみのパターンより統合度が低い
参照 (R) クライアントコンポーネント (高速な初期表示と複雑なUIの両立) サーバープリフェッチ + SWR/TanStack Queryによるハイドレーション SC: await queryClient.prefetchQuery(...) を実行し、dehydrate(queryClient) をPropsとして渡す。CC: <HydrationBoundary> でラップし、useQuery(...) を使用 両者の長所:高速な初期ロード(SSR)とリッチなクライアントサイドのインタラクティビティを両立。優れたUX。初回ロード時のクライアントフェッチを回避 最も複雑:サーバーとクライアントライブラリ両方の概念を理解する必要がある。セットアップが煩雑になる
作成 (C) サーバー/クライアントコンポーネント内のフォーム Server Action <form action={createItemAction}>...</form>。アクションは 'use server' で定義 優れたDX:高い統合度、エンドツーエンドの型安全性。プログレッシブエンハンスメント:JSなしでも動作。統合された状態管理:useActionState でバリデーション/エラーを処理 POSTのみ。公開/外部APIには不向き。Next.jsアプリに密結合する
更新 (U) サーバー/クライアントコンポーネント内のフォーム Server Action <form action={updateItemAction.bind(null, itemId)}>...</form>.bind で追加引数を渡す 作成時と同様。オプティミスティックUI:useOptimistic がUIを即時更新し、優れたUXを提供 作成時と同様
削除 (D) サーバー/クライアントコンポーネント内のボタン/フォーム Server Action <form action={deleteItemAction.bind(null, itemId)}> <button>Delete</button> </form> または onClick ハンドラからアクションを呼び出す 作成/更新時と同様。シンプルで直接的。統合された再検証(revalidatePath)が鍵 作成時と同様
C/U/D 外部サービス (例: Webhook、モバイルアプリ) Route Handler app/api/.../route.ts 内で export async function POST(req) {...}, export async function PUT(req) {...} 等を定義 標準的で相互運用可能:真のRESTfulエンドポイント。全てのHTTPメソッドをサポート。あらゆるサービスから呼び出し可能 自身のUIから呼び出す場合、手動でのクライアントフェッチと状態管理が必要。Server Actionsより統合度が低い

第5章 戦略的推奨事項とアーキテクチャ設計

本レポートで分析した知識を実践に活かすため、この最終章では、開発者が効果的なアプリケーションを構築するための具体的なアドバイスと高レベルな設計パターンを提供します。

5.1 API実装のための意思決定フレームワーク

開発者が直面するであろう選択肢を導くための、シンプルな意思決定フローチャートを以下に示します。

  1. この操作は「参照(READ)」か、それとも「更新(C/U/D)」か?
    • もし「参照」ならば:
      • そのデータは、ページの初期表示のための静的なものか?
        • はいサーバーコンポーネント内でサーバーサイドfetch を使用する。キャッシュ戦略(force-cache vs. no-store)を決定する。
        • いいえ(クライアントのインタラクション用)→ そのページは、このデータと共に高速な初期表示が必要か?
          • はいサーバープリフェッチ + ハイドレーション パターンを使用する。
          • いいえ(ページがインタラクティブになった後にロードしても良い)→ Route Handler + クライアントライブラリ(SWR/TanStack Query) を使用する。
    • もし「更新」ならば:
      • このエンドポイントの「利用者」は誰か?
        • 自身のNext.jsアプリケーションのUIフォーム、ボタン)→ Server Action を使用する。
        • 外部のサービスWebhook、モバイルアプリ、サードパーティ)→ Route Handler を使用する。

このフレームワークは、操作の種類とデータの要件に基づいて、最も適切なアーキテクチャパターンを選択するための明確な道筋を提供します。

5.2 アプリケーション構造のベストプラクティス

一貫性と保守性の高いアプリケーションを構築するための、構造に関する推奨事項です。

  • データフェッチロジックの配置:
    • データ取得関数は、それを使用するコンポーネントの近くに配置するか、あるいは中央集権的なapp/lib/data.tsのようなファイルにまとめることが推奨されます。
    • サーバーサイド専用のデータ取得ロジックが誤ってクライアントバンドルに含まれることを防ぐため、server-onlyパッケージを積極的に利用してください。これにより、ビルド時にエラーが発生し、意図しない情報の漏洩やパフォーマンスの低下を防ぐことができます。
  • Server Actionsの配置:
    • 再利用可能なServer Actionsは、app/actions.tsというファイルに一元管理することが望ましいです。これにより、更新ロジックが整理され、どのコンポーネントからでも簡単にインポートして利用できるようになります。
  • コンポーネントの構成:
    • 開発は常にサーバーコンポーネントをデフォルトとして開始してください。app/ui/app/components/といったディレクトリを作成し、コンポーネントを整理します。
    • 'use client'ディレクティブは、状態管理やイベントリスナーといったインタラクティビティが真に必要になった場合にのみ追加します。
    • レイアウトを設計する際は、インタラクティブなコンポーネントをchildrenとして受け入れる構造にすることで、クライアントバンドルのサイズを最小限に抑え、アプリケーション全体のパフォーマンスを最大化するよう努めてください。

5.3 Next.jsにおけるデータの未来

Next.js App Routerが示す方向性は、フロントエンドとバックエンドの境界がますます曖昧になり、より統一されたフルスタックのReactフレームワークへと進化していく未来です。

Server Actionsが、将来的により汎用的な「Server Functions」へと進化し、POST以外のHTTPメソッドもサポートするようになれば、アプリケーション内部の通信におけるRoute Handlersの必要性はさらに減少する可能性があります。

React本体の機能(useフック、cache)とNext.jsの機能(RSC、拡張fetch)の継続的な統合は、今後さらにシームレスで強力なデータハンドリングモデルが生まれることを示唆しています。開発者は、これらの進化するパラダイムを追い続け、アプリケーションのパフォーマンス、開発者体験、そしてユーザーエクスペリエンスを最大化するための最適なアーキテクチャを常に模索していく必要があります。App Routerは、そのための強力な基盤を提供しており、本レポートで詳述したパターンを理解し、適用することが、その能力を最大限に引き出す鍵となるでしょう。

Appendix: Next.js API 関連にフォーカスした参考的なディレクトリ構成

この構成は、レポートで強調された「関心の分離」「スケーラビリティ」「Server ActionsとRoute Handlersの戦略的な使い分け」を具現化するものです。

API関連のディレクトリ構成

以下に、API関連のロジックを整理するための推奨ディレクトリ構成をツリー形式で示します。

.
└── app/
    ├── api/
    │   ├── webhooks/
    │   │   └── stripe/
    │   │       └── route.ts  // (Route Handler) StripeからのWebhook受信用
    │   └── v1/
    │       └── posts/
    │           └── route.ts  // (Route Handler) 外部公開用GET /api/v1/posts
    │
    ├── lib/
    │   ├── actions/
    │   │   ├── _base.ts          // (任意) アクションで共通の型や認証処理
    │   │   ├── posts.ts          // 'use server'; (Server Actions) 投稿の作成・更新・削除
    │   │   └── auth.ts           // 'use server'; (Server Actions) ログイン、ログアウト処理
    │   │
    │   └── data/
    │       ├── posts.ts          // (Data Access) DBからの投稿データ取得・更新関数
    │       └── users.ts          // (Data Access) DBからのユーザーデータ取得関数
    │
    └── (components, pages, etc...)

各ディレクトリ・ファイルの役割解説

1. app/api/ - 【外部向け】Route Handlers

  • 目的: アプリケーションの外部に公開するAPIエンドポイント(Route Handlers)を配置します。
  • ユースケース:
    • モバイルアプリやサードパーティサービスが利用するRESTful API。
    • StripeやGitHubなど、外部サービスからのWebhookを受信するエンドポイント。
  • ポイント:
    • Next.jsの規約通り、URLセグメント(例: /api/v1/posts)とディレクトリ構造が一致します。
    • v1のようにバージョンをパスに含めることで、将来的なAPIの変更に対応しやすくなります。
    • このディレクトリ内の処理は、レポートの結論通り「自身のNext.jsアプリ以外からのリクエスト」を処理するために特化させます。

2. app/lib/ - 【内部向け】共有ロジック

アプリケーション全体で共有される、UIから分離されたビジネスロジックやデータアクセス層を配置します。lib(libraryの略)はこのような役割に最適な、一般的な命名規則です。

a) app/lib/actions/ - 【内部向け更新処理】Server Actions
  • 目的: **アプリケーション内部のUI(フォームやボタン)**から呼び出されるデータ更新処理(Server Actions)を一元管理します。
  • ポイント:
    • 機能ごとのファイル分割: アプリケーションがスケールすると単一のactions.tsファイルは肥大化します。posts.ts, auth.ts のように機能ドメインでファイルを分割することで、高い保守性を維持できます。
    • 'use server';: 各ファイルの先頭にこのディレクティブを記述することで、ファイル内のすべてのエクスポートされた関数がServer Actionになります。
    • 命名規則: createPost, updateUserSettings のように、関数名がその操作内容を明確に表すようにします。
b) app/lib/data/ - 【内部向けデータアクセス層】
  • 目的: データベースへのクエリや外部APIへのfetchといった、純粋なデータアクセスロジックをカプセル化します。
  • ポイント:
    • ロジックの集約: サーバーコンポーネントやServer Actionsから、このディレクトリ内の関数を呼び出してデータを操作します。これにより、データソース(DB、ORM、APIなど)への依存をこの層に限定できます。
    • server-onlyの活用: このディレクトリ内の関数はサーバーサイドでのみ実行されるべきです。import 'server-only'; をファイルの先頭に追加することで、誤ってクライアントコンポーネントからインポートされた場合にビルドエラーを発生させ、機密情報(DB接続情報など)の漏洩を未然に防ぎます。
    • : getPublishedPosts(), getUserById(id) のような関数を定義します。

この構成がもたらすメリット

  • 明確な関心の分離:
    • app/api/: 外部とのインターフェース。
    • app/lib/actions/: 内部UIからの更新ロジック。
    • app/lib/data/: データソースとのやり取り。
    • コンポーネント: UIのレンダリング。
  • 高い保守性とスケーラビリティ: 機能が増えても、関連するファイルを適切なディレクトリに追加するだけで済み、コードベースの見通しが良く保たれます。
  • 安全性の向上: server-onlyパッケージの活用により、サーバーサイド専用コードがクライアントバンドルに含まれるリスクを排除します。
  • 開発効率: どこに何を書くべきかが明確なため、チーム開発においても一貫したアーキテクチャを維持しやすくなります。

このディレクトリ構成は、レポートで詳述されたNext.js App Routerの設計思想を実践するための、堅牢で拡張性の高い土台となります。

Discussion