nextjs-app-architecture について
Next.js 16+ の App Router アプリを構築・監査する開発者向けに、機能単位の RSC アーキテクチャを案内するワークフローです。クエリ、コンポーネント、ページ境界、操作の設計を進め、確認用チェックリストも提供します。
主な機能
- 01機能単位でのコードとクエリの配置
- 02同期的なページ構成
- 03Suspense による読み込み境界の管理
- 04サーバー・クライアントコンポーネントの分離
- 05アーキテクチャ監査チェックリスト
活用シーン
- 01Next.js アプリのアーキテクチャを新規構築
- 02App Router プロジェクトの監査やリファクタリング
- 03データクエリとサーバーアクションの配置
- 04読み込みや操作の実装確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ aurorascharff/ nextjs-app-architecture-skill --skill nextjs-app-architectureインストール後、Claude Code で/nextjs-app-architectureを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSEmetadata.jsonREADME.mdreferences/feature-folders.mdexample.mdpages-suspense.mdinstant-navigation.mdqueries-actions.mdcache-components.mdcomponents.mdsingle-page-applications.mdtesting.mdux-patterns.md詳細
Next.js アプリのアーキテクチャ
next-beats のように、一貫した機能スライス型の RSC アーキテクチャに従う Next.js 16 以降の App Router アプリを構築・監査するためのワークフローです。
以下のワークフローを順を追って進めてください。これにより、構築の過程で不変条件が満たされます。判断に必要な内容を参照するため、各ステップで指定されたリファレンスを読み込んでください。フレームワークの 仕組み(API のシグネチャ、設定オプション、フックの契約)はリンク先のドキュメントで確認し、内容を言い換えたり、推測で補ったりしないでください。Canary 版限定の Next.js API のリンクには preview.nextjs.org を使用します。
前提条件
Next.js アプリを変更する前に、AI エージェントがバージョンに合ったドキュメントを参照できるよう、プロジェクトが設定されていることを確認してください。AI Coding Agents guide に従い、プロジェクトの AGENTS.md または同梱ドキュメントを優先し、存在しない場合は作成または更新してください。その後、アーキテクチャの判断にこのスキルを使用します。
アーキテクチャの目標
ルートローダーのように振る舞うのではなく、読み込み時の体験を記述するページを構築します。
app/**/page.tsxとlayout.tsxは、同期的な構成サーフェスです。静的な外枠、セクション見出し、<Suspense>境界、エラー境界、トランジションラッパーを配置します。- 機能コンポーネントはサーバー側でデータを読み込みます。受け取るのは最小限で安定した入力(
id、slug、handle、解析済みのフィルター値)または取得済みのレコードであり、生のparams/searchParamsは受け取りません。 - クエリとアクションは機能フォルダーに配置します。コンポーネントはクエリをインポートし、クライアント側のリーフコンポーネントはアクションを直接インポートします。
- サーバーのタグとクライアントのクエリキーが同じ機能データを表す場合、それらの識別子は、機能内に配置された副作用のないキャッシュ契約によって管理します。
- 安定した外枠、ラッパー、スケルトンでレイアウトを維持します。カードやパネルは Suspense の外に配置し、フォールバックで切り替えるのはデータに依存する本文のみです。
不変条件(すべての変更で満たすべきこと)
譲れない条件です。ワークフローでこれらを実現し、最終チェックで検証します。
- ページは構成するだけで、データを取得しません。 ページ/レイアウトでは機能コンポーネントをインポートし、
<Suspense>を配置します。クエリやドメインロジックをインラインで記述しません。connection()、redirect()、または解決済みのルートプロパティを囲む境界を配置する目的に限り、小規模なルートローカルの制御フローヘルパーを使用できます。 - ページは同期処理を維持します。
params.then()、searchParams.then()、またはPromise.all([params, searchParams]).then(...)を使用し、先頭でawait paramsは使用しません。これにより、シェルの静的部分にクロームが描画され、データに依存するセクションだけがサスペンドします。 - 機能コンポーネントにはルートプロパティではなく ID を渡します。 ページの境界で
params/searchParamsを解決し、プレーンな値(id、slug、query)を機能コンポーネントに渡します。 - 非同期サーバーコンポーネントをデフォルトにします。
'use client'を使用するのは、フック、イベントハンドラー、またはブラウザー API が必要な場合に限り、しかもリーフでのみ使用します。サーバーコンテンツの親には使用しません。 - Suspense 境界はページが所有し、スケルトンは機能コンポーネントが所有します。 機能コンポーネント側で
<Suspense>によるラップは行いません。安定したラッパー/カード/クロームで境界を囲み、フォールバックと最終コンテンツで重複させません。 - スケルトンはコンポーネントと同じファイルに置き、コンポーネントと併せてエクスポートし、ファイル末尾で定義します。
FeedとFeedSkeletonは兄弟関係にします。 - クエリは
<domain>-queries.tsに置きます(import 'server-only')。アクションは<domain>-actions.tsに置きます('use server')。サブコンセプトの場合も含め、ファイル名はフォルダー名と一致させます。 - 機能フォルダーはプロダクトの所有範囲に従います。 エンティティが所有するサブコンセプト(favorite、like、vote、bookmark)は親に統合します。複数のドメインを構成するルートレベルのエクスペリエンスは、UI と状態を別の機能に持たせることができます。その場合も、各ドメインは自身のクエリとアクションを保持します。
- クライアントコンポーネントからアクションを直接インポートします。 サーバーアクションを呼び出すだけの目的で、プロパティとして渡すことはしません。
- 機能ローカルのキャッシュ連携は、そのドメイン内に保ちます。 純粋なタグ/キーは
<domain>-cache.tsに、クライアントのクエリ定義は<domain>-query-options.tsに、フックラッパーはhooks/use-*.tsに、小規模なクライアントリーフはcomponents/に配置します。サポートコードを共通化するのは、実際に複数の機能で再利用されてからにします。 - インタラクティブな非同期 UI では、サーバーデータをサーバーに置き、クライアント状態はインタラクションに必要な範囲に限定します。 状態を複製したプロパティ、派生状態を更新するエフェクト、手作りの保留中配列の代わりに、
useOptimistic、useTransition、リデューサー、URL/検索パラメーター、フォームアクションを使用します。
ワークフロー
ゼロからの構築、機能開発、監査では、以下を順番に実施します。各手順には、参照先と合格すべきチェック項目を記載しています。
- モードを選択します。
- ゼロから構築する場合: コードを書く前に、ルート、実際のドメイン名詞、静的シェル、想定されるローディンググループをスケッチします。
- 監査/リファクタリングの場合: まず現在の
app/ページを確認し、すべての非同期ページ、ページレベルのクエリインポート、ルートプロパティの漏出、欠落している Suspense 境界、機能フォルダーの不一致を列挙します。 → 目指す形についてはreferences/example.md、配置についてはreferences/feature-folders.mdを参照します。 ✓ アーキテクチャを新たに作成するのか、ローダー形式のコードをこの構成に変換するのかを把握しています。
- 作業を配置します。 何かを書く前に、機能フォルダーを決定します。
→
references/feature-folders.md(判断ツリーと統合ルール)を参照します。 ✓ 実際のドメイン、複数ドメインにまたがるプロダクト体験、または適切な親への統合のいずれかです。 - クエリを記述し、クライアントキャッシュでデータを共有する場合はキャッシュ契約も定義します。 サーバー読み取りは
import 'server-only'を含めて<domain>-queries.tsに記述し、共有タグ/キーの識別子は純粋な<domain>-cache.tsに保持します。 →references/queries-actions.mdを参照します。SWR/TanStack Query を使う場合はreferences/single-page-applications.mdを、cacheComponents: trueの場合はreferences/cache-components.mdも参照します。 ✓ キャッシュ識別子は一度だけ定義し、サーバー読み取りはサーバー専用とします。また、Cache Components のもとでキャッシュ、タグ付け、ライフタイム管理を行い、ORM の行ではなくドメイン型を返します。 - アクションを記述します(ミューテーションがある場合)。
features/<domain>/<domain>-actions.tsに配置し、先頭に'use server'を記述します。 →references/queries-actions.mdを参照します。 ✓ 認証を再確認し、入力を検証し、Cache Components のもとで対応するキャッシュタグを無効化し(refresh()は正当な動的読み取りの場合に限って使用)、判別可能なユニオンを返します。 - コンポーネントとスケルトンを構築します。
features/<domain>/components/<name>.tsxに、最小限のプロパティを受け取り、自身のクエリを await する非同期サーバーコンポーネントを作成します。'use client'はインタラクティブな末端コンポーネントにのみ使用します。 →references/components.mdを参照します。クライアントデータライブラリを使う機能、または厳密な SPA/CSR 機能の場合はreferences/single-page-applications.mdも参照します。 ✓ コンポーネントはparamsではなく、ID/ハンドル/解析済みフィルター、またはすでに解決済みのレコードを受け取ります。スケルトンは末尾で兄弟エクスポートとして定義し、別名のスケルトンラッパーは作成しません。 - ページを構成します。
app/<route>/page.tsxは同期的にし、params.then()/Promise.all(...).then(...)を使用します。データを表示する箇所を Suspense で囲み、独立して失敗する可能性のあるセクションはエラーバウンダリーで囲みます。 →references/pages-suspense.mdを参照します。cacheComponents: trueの場合は、どのリンクがプリフェッチされ、どの読み取りがナビゲーションを待つかについてreferences/instant-navigation.mdも参照します。 ✓ ルートプロパティは通常の値に変換します。レイアウトを設定する安定したカード/セクションは Suspense で囲みます。境界はページ上で確認できる状態にします。 - インタラクションを追加します(必要な場合):楽観的更新、処理中の状態、トースト、確認。
→
references/ux-patterns.mdを参照します。 ✓ フィードバックが重複しないようにします。楽観的なリデューサー/アクションは機能と一緒に配置します。共有可能な状態は URL/検索パラメーターで管理し、クライアントのエフェクトは導出された React の状態ではなく、外部システムとの同期に使用します。 - 完了を宣言する前に、以下のチェックリストに照らして検証します。ローディングの流れに変更を加えた場合は、エンドツーエンドテストで確実に固定します。
→
references/testing.mdを参照します。 ✓ 未キャッシュの処理が完了する前に、どのコンテンツが利用可能かを可視性を考慮したロケーターで検証します。
完了前の確認
変更したファイルを読んで確認できるすべての不変条件について、差分を検査します。
- ページ/レイアウトは
*-queriesファイルをインポートせず、再利用可能なドメイン UI をインラインで定義しません。インラインヘルパーがある場合は、ルート固有の制御フローに限られます。 -
paramsを持つページはすべて同期的で、params.then()/searchParams.then()/Promise.all([params, searchParams]).then(...)を使用します。 - フィーチャーコンポーネントには、プレーンな ID/ハンドル/解析済みフィルター、または解決済みレコードを渡します。フィーチャーの props に
paramsまたはsearchParamsという名前は使いません。 - ページデータ用の
<Suspense>はすべてページ内に配置します。フィーチャーが自身を事前にラップすることはありません。 - 安定したラッパー/カード/クロームは Suspense の外側に配置します。フォールバックと最終コンテンツで、同じ外側のカードを重複させません。
- すべてのコンポーネントは、実際の
*Skeletonを同じファイル内の末尾に定義します。props を渡すためだけの小さなスケルトン別名は作りません。 - スケルトンとコンテンツの高さは同じであること(実測済み)。セクション見出しは境界の外側に配置し、空になる可能性のあるセクションは領域を確保します。
- 高さが変動するセクションより下のセクションは、そのセクション内に兄弟境界としてネストします。そのセクションで待機したり、子として渡したりしません。
-
unstable_prefetch()/unstable_navigation()は、クエリ呼び出し前にコンポーネント内で await します。ゲートを保持するだけのラッパークエリは作りません。 - すべての
*-queries.tsはimport 'server-only'で始まり、すべての*-actions.tsは'use server'で始まります。 -
cacheComponents: trueの場合、再利用可能な読み取りには'use cache'/cacheTag/cacheLifeを使用し、適切な場合は'use cache: private'/'use cache: remote'を使用します。動的な読み取りは意図的であり、その理由が明確になっています。 - パーソナライズされた共有キャッシュのスコープには、キャッシュされないフィーチャーラッパーから、信頼できる正規化済みの ID 値を渡します。キャッシュスコープ内でリクエスト API を読み取ったり、クライアント提供の ID を信頼したりしません。
- すべての書き込みで
updateTag()を呼び出す読み取りにはcacheLife('max')を使用します。より短いプロファイルは、アプリのアクション以外で変更されるデータに限ります。 - キャッシュ済み読み取りに影響する変更では、対応するタグに対して
updateTag()/revalidateTag(..., 'max')を呼び出します。refresh()はタグの無効化の代用にはなりません。 -
prefetch={true}は、ユーザーが予測可能なリンクをたどる場合、またはunstable_navigation()の背後に重い処理を置いて、行ごとのリンクのプリフェッチを低コストにした場合に限って使用します。 - アクションファイルには
<folder>-actions.tsという名前を付けます。エンティティが所有するサブコンセプトのために独自のフォルダーを作らず、複数のドメインにまたがるプロダクト機能がエンティティのクエリ/アクションを所有することもありません。 - サーバータグとクライアントクエリキーの両方があるフィーチャーでは、それらを純粋な
<domain>-cache.ts内で一度だけ定義します。クエリ、アクション、ハイドレーション、クエリオプション、フックはそこからインポートします。 - フィーチャー固有のクライアントサポートファイルは、要件に合う最小の場所に配置します。クエリオプションはフィーチャーのルート、
use-*フックラッパーはhooks/、リーフコンポーネントはcomponents/に置き、共有サポートは実際に複数のフィーチャーで再利用されてから導入します。 -
'use client'コンポーネントはリーフです。非同期サーバーコンポーネントではなく、アクション/フック/プロバイダーをインポートします。サーバーデータが必要なクライアントコンポーネントには、フィーチャー内にサーバー側の半分を用意します(ComposePanel→ComposePanelClient)。そのためのクエリプロミスをページやレイアウトで作成しません。 - クライアントのリーフは、操作に
useOptimistic、トランジション、リデューサー、URL 状態、またはフォームアクションを使用します。派生した React 状態を求めるために、エフェクト内でsetStateを呼び出しません。 - アクションが入力を拒否する可能性のあるフォームでは、ユーザーが入力した値を保持します。アクション状態で送信値を返し、フィールドでは
defaultValueとして読み取ります。 - 変更処理では入力を検証し、影響を受けるデータを無効化します。
リファレンス一覧
references/feature-folders.md— コードの配置先:フォルダー構成、キャッシュの契約、命名、サブコンセプトの統合。references/queries-actions.md— クエリ/アクションのルール:サーバー専用、重複排除、検証、無効化、戻り値の形式。references/components.md— サーバー/クライアントの境界、スケルトン、use()、単一使用のヘルパー、ライブデータ。references/pages-suspense.md— ページの構成、params.then()、Suspense の配置、CLS、エラーバウンダリ。references/instant-navigation.md— プリフェッチの判断:prefetch={true}、3 つのレンダリング段階とその条件、ナビゲーションに表示される内容の検証、非表示ルート。references/cache-components.md—cacheComponentsに関する判断:どの読み取りをキャッシュするか、どのディレクティブを使うか、無効化の方法。references/single-page-applications.md— クライアントキャッシュに関する判断:配置、サーバーによるシード、Cache Components との連携、ハイドレーション、ミューテーション。references/ux-patterns.md— インタラクションに関する判断:楽観的更新と保留中表示、インラインエラーの使い分け、トースト、action-prop、確認。references/testing.md— エンドツーエンドテスト:instant()による読み込みシーケンスの検証、非表示ルート内のコンテンツの検索、ミューテーションのテスト。references/example.md— next-beats のリファレンスアプリ:不変条件からファイル一覧を確認し、ルールが実際のコードでどう使われているかを参照できます。