Next.js プロジェクトの開始
基盤は初日に築くのが最も安上がりです。後からルールを導入しようとすると、それを破っているコードの上に修正を重ねることになります。frontend-starter テンプレートが存在するなら、コピーから始めてください。 テンプレート内では以下の手順がすでに実施・検証済みであり、このドキュメントは決定事項の説明と監査チェックリストとして残ります。
このドキュメントは手順の順序と最小セットを定義します。コードの書き方自体はスキル fsd-frontend(プロジェクトにあれば)、認証は frontend-auth-session、コミット前の確認は anti-slop-review を参照してください。完成済みの設定ファイルは references/configs.md、Next + FSD のコード例付き構成は references/next-structure.md にあります。
ルールの優先順位。 ユーザーの明示的な指示とプロジェクトのエージェントファイル(AGENTS.md、CLAUDE.md)はこのドキュメントよりも優先されます。ライブラリのバージョンは変更されます。設定ファイルをコピーする前に、インストール済みバージョンのドキュメントと照合し、実際に動作確認するまでは正しいと断言しないでください。
Next.js に関する事実は、このドキュメントや記憶からではなく、パッケージ内のドキュメントから取得してください。 16.2 以降、ドキュメントは node_modules/next/dist/docs/ にあり、プロジェクトの AGENTS.md がそれを参照しています。ここにある例と矛盾する場合は、インストール済みバージョンのドキュメントが優先されます。
表記: MUST — 違反不可。SHOULD — デフォルトで従うこと。逸脱には理由が必要。
1. コードを書く前の判断
以下の表の各判断は、理由とともに README(「判断」セクション)または ADR に記録します。デフォルトから逸脱する場合、記録された理由なしには実施しません。
| 質問 | デフォルト | それ以外のケース |
|---|---|---|
| Next.js か SPA か | SSR/SEO、BFF(シークレット、HttpOnly-cookie)、ルートハンドラのうち少なくとも1つが必要なら Next.js | ログイン必須で SEO も BFF も不要な内部アプリケーション → Vite + React Router:ビルドとデプロイが簡単 |
| ルーター | App Router | Pages Router — 既存プロジェクトのみ |
| モノレポ | なし、単一アプリケーション | 2つ目のアプリケーションかパッケージが実際に登場した場合 → pnpm workspaces。Nx/Turborepo — ビルドや CI がボトルネックになり始めた場合 |
| Multi-Zones, マイクロフロントエンド | なし | 独立したチームが独立したリリースを行う場合 |
| サーバーデータ | 初期読み込みは Server Components。インタラクティブなクライアントデータとミューテーションには TanStack Query | 純粋なクライアントアプリケーション → TanStack Query のみ |
| テキストと i18n | 初日から翻訳モジュール(next-intl または同等品)。プロジェクトが fsd-frontend に従う場合、ロケールが1つでも対象 | README に「i18n なし」と明示的に記録 |
| パッケージマネージャー | pnpm、バージョンは packageManager に指定 | — |
| デプロイ | すぐに決定:Vercel、Node サーバー(output: "standalone")、または Docker | これにより env、キャッシュ、CI が変わる |
MUST: 「将来のために」テクノロジーを追加しない。モノレポ、2つ目のルーター、Storybook、状態管理、MSW(「バックエンドの一種として」)は、最初の実際の利用者が現れたときに導入する。
2. 手順の順序
- プロジェクトを作成(
pnpm create next-app@latest):TypeScript、ESLint、Tailwind、App Router を指定。packageManager、engines.node(Next 16 は Node 20.9+ 必須)、.nvmrcを固定。 - 生成テンプレートをすぐに削除: スタートページ、
public/内の SVG、テンプレート README、デモスタイル。README をreferences/configs.mdの骨組みで置き換え。放置したテンプレートはそのまま本番に残る。create-next-appが生成したAGENTS.mdとCLAUDE.mdは削除しない:<!-- BEGIN:nextjs-agent-rules -->と<!-- END:nextjs-agent-rules -->の間のブロックはエージェントにインストール済みバージョンのドキュメントを指し示し、next devが自動で管理する。プロジェクト独自のルールはこれらのマーカーの外に追記する。 - 厳格な TypeScript(
references/configs.mdのセクション 4)とtypecheckスクリプト(tsc --noEmit)。 - FSD 構造を Next 上に構築(後述のセクション 3)。
- リンターとフォーマッター:
eslint-config-nextの ESLint flat config + type-aware ルール + 境界リント + Prettier。Next 16 ではnext lintが削除され、next build時にリントは実行されない。リンターは個別のスクリプトと CI で実行する。 - 環境変数設定(セクション 4)。
- エラーモデルと API クライアントを
shared/apiに作成(セクション 5)。 - プロバイダーを
_appレイヤーに配置:QueryClient、テーマ、i18n、グローバルサービス(トースト、確認ダイアログ)はsharedのポート +_appの実装。 - UI の基礎(セクション 6)。
- テスト: Vitest + Testing Library + MSW、e2e は Playwright(セクション 7)。
- Git フック: husky + lint-staged(eslint --fix、prettier)+ commitlint。
- CI: プッシュと PR ごとに lint、typecheck、test、build を実行(セクション 8)。
- デフォルトのセキュリティ(セクション 9)。
- README を実際の内容で埋め(セクション 10)、Definition of Done を確認(セクション 11)。
手順 1~3 は「scaffold」として個別のコミットにします。これにより、履歴上でジェネレーターが行ったことと自分が行ったことを区別できます。
3. Next ルーティング上の FSD
App Router 向け FSD 公式ガイドに従い、Next のサービスフォルダ app/ はプロジェクトルートに配置し、FSD レイヤーは src/ に配置します。app レイヤーと pages レイヤーにはプレフィックス _ を付け、Next のサービスフォルダと混同しないようにします。
app/ Next ルーティング:ルートファイルのみ
├── layout.tsx ルートレイアウト → src/_app のプロバイダー
├── (auth)/sign-in/page.tsx export { SignInPage as default } from "@/_pages/sign-in"
└── api/auth/login/route.ts ルートハンドラ(BFF)、薄い
proxy.ts Next 16+(以前は middleware.ts)、app/ の隣
src/
├── _app/ app レイヤー:プロバイダー、グローバルスタイル、サービス実装
├── _pages/ pages レイヤー
├── widgets/ (プロジェクトで使用する場合)
├── features/
├── entities/
└── shared/{ui,api,lib,config}
MUST:
app/内のファイルは薄く保つ:src/_pagesからのページ再エクスポート、generateMetadata、params/searchParamsの解析(Next 15+ ではPromiseなのでawait必須)、プロップスでの受け渡しのみ。ロジックは一切含めない。- ルートハンドラと Server Actions は薄く保つ:入力を解析・検証 →
src内の関数を呼び出す → レスポンスを返す。それぞれが公開エントリポイントであり、自身で認証を確認し、proxy.tsに依存しない。 src/appやsrc/pagesを作成しない:Next はルートとsrcの両方でサービスフォルダを探すため、同名のフォルダが2つ目のルーターになり得る。- 境界リントはプレフィックスを認識する:
src/app、src/pages/*の代わりにsrc/_app、src/_pages/*のパターンを使用(設定はfsd-frontend、references/conventions.mdセクション 11)。 "use client"はツリーの可能な限り下位に配置する。ページはデフォルトでサーバーコンポーネントとし、クライアントコンポーネントはインタラクティブなリーフとしてのみ使用する。- シークレットを含むモジュールは
import "server-only"で始め、windowにアクセスするモジュールはimport "client-only"で始める。
詳細とコード例は references/next-structure.md。
4. 環境設定
MUST:
- 変数は2つのモジュールで読み取る:
shared/config/env.server.ts(import "server-only")とshared/config/env.client.ts。スキーマはzodを使用し、インポート時にバリデーション:設定エラーは最初のリクエスト時ではなく起動時に検出される。それ以外のコードではprocess.envを禁止(リンタールール)。 NEXT_PUBLIC_*はビルド時にバンドルに埋め込まれ、process.env.NEXT_PUBLIC_Xの直接参照時にのみ機能する:分割代入やprocess.env[name]は不可。この方法では、異なる公開値を持つ複数の環境に対して単一のビルドイメージを使用できない。そのような値はサーバーで読み取り、下位に渡す。- シークレットは
NEXT_PUBLIC_プレフィックスなしで使用。 .env.exampleには全ての変数をコメントと安全な値とともに含める。.env*は.gitignoreに追加するが、.env.exampleは除く。CI でのビルドは.env.exampleの値で実行する。
5. エラーモデルと API クライアント
MUST:
- エラーは
shared/apiの境界で一度だけAppError(kindを持つ)に正規化する(モデルはfsd-frontend、references/conventions.mdセクション 1)。shared/apiより上位では、Response、ステータスコード、エラーテキストを解析してはならない。 - 単一の
request()関数をfetch上に実装する(または同様にラップした生成クライアントを使用):signalを伝播させ、ネットワークエラーをnetworkに、HTTP ステータスをkindに変換し、レスポンスボディをスキーマで検証する。例はreferences/next-structure.md。 - エラーの種類はコードベースで判定する:ステータス、
extensions.code(GraphQL はエラーでも 200 を返す)、ボディのcode。バックエンドがテキストのみを返す場合は、1つのマッパー内でテキストベースのマッピングを理由コメント付きで行い、テストを記述する。 - バックエンドのレスポンスは境界でドメイン型にマッピングする。DTO が UI に漏れてはならない。
- ユーザーデータはユーザー間でキャッシュしない:セッションに依存するレスポンスに対して
"use cache"/unstable_cacheを使用してはならない。 - スキーマ(OpenAPI、GraphQL)が存在する場合、クライアントはスキーマから生成する。スキーマまたは生成コードはリポジトリに含める:CI が実バックエンドにアクセスしてはならない。
SHOULD: トランスポート上のインターフェース(AuthApi + ファクトリー)は、すでに2つ目の利用者が存在する場合にのみ導入する(fsd-frontend、references/state-and-data.md 2.1)。type: "mock" のような設定値で「未実装」をスローするような仕組みは導入しない。
6. UI の基礎
- デザイントークン — CSS カスタムプロパティ(
:root、ダークテーマ用も)として定義し、Tailwind v4 では@theme経由で接続する。コンポーネント内ではトークンのみを使用。生の HEX や任意の値(text-[#3a3a3a])は、コメント付きの一時的な例外としてのみ許容。 - クラスは存在しなければならない。 shadcn/ui やサンプルからコピーしたコンポーネントには独自のトークン(
bg-card、text-muted-foreground)が付属している。これらのトークンを定義するか、クラスを置き換えること。さもなければ、コンポーネントは静かにスタイルが適用されないままになる。 shared/uiのプリミティブは、アクセシビリティのためにヘッドレスライブラリ(Radix、React Aria)上に構築する。バリアントはcva、クラスの結合はcn(clsx+tailwind-merge)を使用。- フォントは
next/font、画像はnext/image(サイズ指定必須)を使用。images.remotePatternsは明示的に列挙する。 - Storybook は、UI キットが複数のアプリケーションやチームで再利用される場合にのみ導入し、それ以前には導入しない。
7. テスト
- Vitest + Testing Library + jsdom + MSW を使用。MSW の設定は
onUnhandledFrame: "error"(MSW 3。MSW 2 ではonUnhandledRequest)。設定はreferences/configs.md。 - Vitest は
asyncServer Components をレンダリングできない。Server Components 内のロジックは関数に抽出してユニットテストし、ページ自体は e2e でテストする。 - Playwright は 3~10 のクリティカルパスに対して使用。CI での e2e は、予測可能なバックエンド(モックサーバー、テスト用 DB のステージング環境)に対して実行し、テストが他のデータを変更する可能性のある共有ステージング環境は避ける。
- 最初のテストはゼロ日目に記述する:テストインフラが機能することを確認し、テンプレートとなる。
8. CI
MUST: メインブランチへのプッシュと全ての PR に対して:lint、typecheck、test、build を実行する。GitHub Actions のテンプレートは references/configs.md。公開リポジトリでは Actions は無料、プライベートリポジトリには月間の分数枠がある。
- CI でのビルドは
.env.exampleの値で実行し、シークレットは使用しない。 - e2e は、分離されたバックエンドが利用可能な場合にのみ別ジョブとして実行。それまでは、README に CI に e2e がない理由を明記する。
- CI バッジはワークフローと同時に README に追加する。
9. デフォルトのセキュリティ
poweredByHeader: false。next.config.tsのheaders()でヘッダーを設定:X-Content-Type-Options: nosniff、Referrer-Policy: strict-origin-when-cross-origin、X-Frame-Options: DENY(または CSP のframe-ancestors)。CSP と nonce は別タスクだが、「あり / なし とその理由」は即座に記録する。- Server Actions とルートハンドラは公開エンドポイント:エントリのスキーマバリデーションと各エンドポイントでの権限チェックを必須とする。
dangerouslySetInnerHTMLは、サニタイザーと理由コメントがある場合のみ使用。- 個人データやトークンはログに記録しない。
- 認証は
frontend-auth-sessionに従う。
10. README
骨組みは references/configs.md。ルールは1つ:README には、このコミットのコードに対して真実であることだけを書く。 各セクションは、それが説明するコードと同じコミットで更新する。計画は「今後の予定」セクションに分けて記述し、未実施であることを明記する。
11. スタート時の Definition of Done
- ジェネレーターのテンプレートを削除。README は骨組みで、真実が埋められている。
-
packageManager、engines.node、.nvmrcを固定。 - TS strict が有効。
typecheck、lint、test、buildがローカルで成功する(実行済みであり、推測ではない)。 - FSD 構成で
_app/_pagesプレフィックスを使用。境界リントが上位へのインポートを検出する。 - 環境変数を2つのモジュールとスキーマで管理。
process.envのそれ以外での使用をリンターで禁止。.env.exampleが完全。 -
AppErrorとrequest()をshared/apiに実装し、テストでカバー(ネットワーク、ステータス、不正なスキーマ)。 -
_appにプロバイダーを配置。サーバー上の QueryClient はユーザー間で共有しない。 - デザイントークンを CSS カスタムプロパティで定義。使用されている全てのクラスが存在する。
- Vitest + MSW + Playwright がセットアップされ、それぞれ少なくとも1つのテストが動作する。
- Git フックと commitlint が動作する。
- CI がグリーン。
- セキュリティヘッダーと
poweredByHeader: falseを設定。 - セクション1の判断が記録されている。
12. 迷ったときは
- 「今追加する」と「必要なときに追加する」の間では — 必要なときに。
- 「手動で設定する」と「create-next-app が生成したままにする」の間では — 変更する理由が生じるまではそのままにし、変更するときは理由を記録する。
- 「単一アプリケーション」と「モノレポ」の間では — 単一アプリケーション。
- 「記憶の設定」と「現在のバージョンのドキュメントの設定」の間では — ドキュメントから。