対象: Next.js 16 系、React 19、App Router 目的: 新規開発・既存プロジェクトのレビュー時に参照するチェックリスト兼ガイド


目次

  1. プロジェクト構成
  2. Server Components と Client Components
  3. データ取得
  4. キャッシュと再検証(Cache Components)
  5. ミューテーション(Server Actions)
  6. ルーティングと UI 状態
  7. Proxy(旧 Middleware)
  8. パフォーマンス最適化
  9. SEO とメタデータ
  10. セキュリティ
  11. 型安全性とコード品質
  12. テスト
  13. デプロイと運用
  14. アンチパターン集
  15. レビューチェックリスト

1. プロジェクト構成

推奨ディレクトリ構成

src/
├── app/                      # ルーティング専用(薄く保つ)
│   ├── (marketing)/          # Route Group: URL に影響しないグルーピング
│   │   ├── page.tsx
│   │   └── layout.tsx
│   ├── (app)/
│   │   ├── dashboard/
│   │   │   ├── page.tsx
│   │   │   ├── loading.tsx
│   │   │   ├── error.tsx
│   │   │   └── _components/  # このルート専用のコンポーネント(_ でルート対象外)
│   │   └── layout.tsx
│   ├── api/                  # Route Handlers(外部公開APIが必要な場合のみ)
│   ├── layout.tsx            # ルートレイアウト
│   ├── not-found.tsx
│   └── global-error.tsx
├── components/               # 共有 UI コンポーネント
│   └── ui/
├── lib/                      # ユーティリティ、外部クライアント
├── server/                   # サーバー専用ロジック(DB、認証、データアクセス層)
│   ├── db.ts
│   ├── auth.ts
│   └── data/                 # データアクセス関数(DAL)
├── actions/                  # Server Actions
└── proxy.ts                  # 旧 middleware.ts

原則

  • app/ はルーティングに専念させる。 ビジネスロジックは server/ や lib/ に置き、page.tsx は「データを取得して組み立てる」だけにする。
  • コロケーション: 特定ルート専用のコンポーネントは _components/ のようなプライベートフォルダでルートの近くに置く。
  • Route Groups (name) でレイアウトを分ける(例: マーケティングページとログイン後のアプリでレイアウトを分離)。
  • src/ ディレクトリを使うと設定ファイルとアプリコードが分離でき見通しが良い。
  • パスエイリアス(@/*)を tsconfig.json で設定し、深い相対パスを避ける。

2. Server Components と Client Components

デフォルトは Server Component

App Router ではすべてのコンポーネントがデフォルトで Server Component。"use client" は必要な場所にだけ、できるだけ末端(葉)に付ける。

Server Component を使うClient Component を使う
データ取得useState / useEffect などのフック
DB・シークレットへのアクセスイベントハンドラ(onClick など)
大きな依存ライブラリの使用(JS をクライアントに送らない)ブラウザ API(window, localStorage)
静的なマークアップインタラクティブな UI

ベストプラクティス

// ❌ ページ全体を Client Component にしている
"use client";
export default function Page() { /* ... */ }
 
// ✅ インタラクティブな部分だけを切り出す
// app/products/page.tsx(Server Component)
import { LikeButton } from "./_components/like-button";
 
export default async function Page() {
  const products = await getProducts();
  return products.map((p) => (
    <article key={p.id}>
      <h2>{p.name}</h2>
      <LikeButton productId={p.id} /> {/* ここだけ "use client" */}
    </article>
  ));
}
  • Server Component を Client Component の children として渡すことで、Client の内側にも Server のツリーを保てる。
  • Props はシリアライズ可能な値のみ。関数・クラスインスタンス・Date は渡す前に変換する(Server Actions は例外的に渡せる)。
  • サーバー専用モジュールには import "server-only" を書き、誤ってクライアントにバンドルされるのを防ぐ。
  • Context Provider は Client Component として作り、ルートレイアウトで children を包む。Provider はできるだけ深い位置に置く。

3. データ取得

Server Component で直接取得する

// app/posts/[id]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params; // Next.js 15 以降 params は Promise
  const post = await getPost(id);
  if (!post) notFound();
  return <PostView post={post} />;
}

原則

  • 自分のアプリの Route Handler を Server Component から fetch しない。 余計なネットワーク往復になる。データアクセス関数を直接呼ぶ。

  • ウォーターフォールを避ける: 依存関係のないデータは Promise.all で並列取得する。

    const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);
  • Suspense でストリーミング: 遅いデータは別コンポーネントに分け <Suspense> で囲み、ページ全体のブロックを避ける。

  • リクエスト内の重複排除: 同じデータを複数コンポーネントで使う場合は React の cache() でラップする。

    import { cache } from "react";
    export const getUser = cache(async (id: string) => db.user.findUnique({ where: { id } }));
  • データアクセス層(DAL)を作る: 認可チェックと DTO 変換(必要なフィールドだけ返す)を DAL に集約する。

    // server/data/posts.ts
    import "server-only";
    import { cache } from "react";
    import { getSession } from "@/server/auth";
    import { db } from "@/server/db";
     
    export const getPost = cache(async (id: string) => {
      const session = await getSession();
      if (!session) return null;                       // 認証
     
      const post = await db.post.findUnique({ where: { id } });
      if (!post || post.authorId !== session.userId) return null; // 認可
     
      // DTO: クライアントに渡してよいフィールドだけ返す
      return { id: post.id, title: post.title, body: post.body, createdAt: post.createdAt.toISOString() };
    });
    • server-only で誤インポートを防ぎ、cache() でリクエスト内の重複呼び出しをまとめる。
    • ページ・レイアウト・generateMetadata・Server Action はこの関数を経由してデータに触れる。DB を直接呼ばない。
    • レイアウトだけで認可チェックをしない(レイアウトはナビゲーション時に再実行されないことがある)。
  • Dynamic API は非同期: cookies(), headers(), draftMode(), params, searchParams はすべて await が必要。

  • クライアント側でのデータ取得が必要な場合(リアルタイム更新、無限スクロールなど)は SWR / TanStack Query を使う。


4. キャッシュと再検証(Cache Components)

Next.js 16 では Cache Components によりキャッシュが「明示的なオプトイン」になった。デフォルトは動的で、キャッシュしたい箇所に "use cache" を書く。

有効化

// next.config.ts
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  cacheComponents: true,
};
export default nextConfig;

使い方

import { cacheLife, cacheTag } from "next/cache";
 
export async function getProducts() {
  "use cache";
  cacheLife("hours");      // 事前定義プロファイル: seconds/minutes/hours/days/weeks/max
  cacheTag("products");    // タグで後から無効化できる
  return db.product.findMany();
}
  • "use cache" はファイル・コンポーネント・関数単位で付けられる。
  • キャッシュされた部分は静的シェルとして事前レンダリングされ、動的な部分は <Suspense> 内でストリーミングされる(Partial Prerendering)。
  • キャッシュ関数の中で cookies() / headers() を直接呼ばない。 必要な値は外で読み取って引数として渡す(引数はキャッシュキーの一部になる)。

再検証

API用途
updateTag(tag)Server Action 内で使用。書き込み直後にユーザーが変更を即座に見られる(read-your-writes)
revalidateTag(tag, profile)stale-while-revalidate で再検証(Next.js 16 では第2引数に cacheLife プロファイルを指定)
revalidatePath(path)パス単位で無効化
refresh()Server Action からキャッシュされていないデータのみを更新

原則

  • ユーザー固有データはキャッシュしない(または "use cache: private" などの適切なスコープを使う)。共有キャッシュへの個人情報混入は重大インシデントになる。
  • タグ設計を最初に決める(例: products, product:${id}, user:${id}:orders)。
  • キャッシュは「速くするため」ではなく「何をどれだけ古くしてよいか」の仕様として決める。

5. ミューテーション(Server Actions)

// actions/post.ts
"use server";
 
import { z } from "zod";
import { updateTag } from "next/cache";
import { redirect } from "next/navigation";
import { getSession } from "@/server/auth";
 
const schema = z.object({
  title: z.string().min(1).max(100),
  body: z.string().max(10_000),
});
 
export async function createPost(prevState: unknown, formData: FormData) {
  // 1. 認証・認可(Server Action は公開エンドポイントと同じ扱い)
  const session = await getSession();
  if (!session) return { error: "ログインが必要です" };
 
  // 2. 入力検証
  const parsed = schema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) return { error: parsed.error.flatten().fieldErrors };
 
  // 3. 処理
  const post = await db.post.create({ data: { ...parsed.data, authorId: session.userId } });
 
  // 4. キャッシュ更新とリダイレクト
  updateTag("posts");
  redirect(`/posts/${post.id}`);
}
// Client 側
"use client";
import { useActionState } from "react";
import { createPost } from "@/actions/post";
 
export function PostForm() {
  const [state, action, pending] = useActionState(createPost, null);
  return (
    <form action={action}>
      <input name="title" />
      <textarea name="body" />
      <button disabled={pending}>{pending ? "送信中…" : "投稿"}</button>
      {state?.error && <p role="alert">{String(state.error)}</p>}
    </form>
  );
}

原則

  • Server Action は誰でも直接呼び出せる公開 API として扱う。 毎回、認証・認可・入力検証を行う。

  • 入力検証には Zod / Valibot などのスキーマライブラリを使う。

  • 期待されるエラー(バリデーション失敗など)は 戻り値で返す。想定外のエラーは throw して error.tsx に任せる。

  • useActionState(状態)・useFormStatus(送信中表示)・useOptimistic(楽観的更新)を活用する。

  • <form action={...}> を使うと JS 読み込み前でも動作する(プログレッシブエンハンスメント)。

  • データ取得には Server Action を使わない(POST で直列実行されるため)。取得は Server Component で行う。

  • redirect() を try/catch の中で呼ばない。 redirect() / notFound() は内部で例外を投げて動作するため、catch に捕まると遷移しなくなる。処理を try/catch で囲み、redirect() はその外で呼ぶ。

    let post;
    try {
      post = await db.post.create({ data });
    } catch {
      return { error: "保存に失敗しました" };
    }
    redirect(`/posts/${post.id}`); // try/catch の外

Route Handlers との使い分け

用途使うもの
アプリ内のフォーム送信・データ更新Server Action
Webhook(Stripe、GitHub など)Route Handler
外部・モバイルアプリ向けの APIRoute Handler
ファイル配信、RSS、ストリーミングレスポンスRoute Handler
  • Webhook は 署名を必ず検証 する(未検証のリクエストを処理しない)。
  • 外部公開の Route Handler には レート制限 を入れる(Upstash Ratelimit、API Gateway など)。
  • Route Handler も Server Action と同じく、毎回認証・認可・入力検証を行う。

6. ルーティングと UI 状態

特殊ファイルを活用する

ファイル役割
loading.tsxルート単位のローディング UI(自動で Suspense 境界になる)
error.tsxルート単位のエラー境界("use client" 必須)
not-found.tsxnotFound() 呼び出し時の UI
global-error.tsxルートレイアウトのエラーを捕捉
default.tsxParallel Routes のフォールバック(Next.js 16 では必須)
forbidden.tsx / unauthorized.tsxforbidden()(403)/ unauthorized()(401)呼び出し時の UI(experimental.authInterrupts で有効化)
  • 権限がない場合は notFound() で隠すか forbidden() で 403 を返すかを方針として決めておく(リソースの存在自体を隠したい場合は notFound())。

ナビゲーション

  • 内部リンクは必ず <Link> を使う(プリフェッチとクライアント遷移が効く)。
  • プログラム遷移は useRouter()(Client)、サーバー側では redirect() / permanentRedirect()。
  • URL を状態として使う: フィルタ・ソート・ページ番号・タブなどは searchParams に持たせると、共有・ブックマーク・SSR が容易になる(nuqs などのライブラリも有用)。
  • モーダルは Parallel Routes + Intercepting Routes で実装すると URL 共有とリロードに強くなる。

7. Proxy(旧 Middleware)

Next.js 16 で middleware.ts は proxy.ts に名称変更され、Node.js ランタイムで動作する。

// src/proxy.ts
import { NextResponse, type NextRequest } from "next/server";
 
export function proxy(request: NextRequest) {
  const hasSession = request.cookies.has("session");
  if (!hasSession && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return NextResponse.next();
}
 
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

原則

  • Proxy はリダイレクト・リライト・ヘッダー付与・楽観的な認証チェックなどの軽い処理に限定する。
  • Proxy だけで認可を完結させない。 実際の認可はデータアクセス層・Server Action でも必ず行う(多層防御)。
  • matcher で静的アセットを除外し、不要な実行を避ける。
  • 重い DB アクセスを Proxy に入れない(全リクエストのレイテンシに乗る)。

8. パフォーマンス最適化

画像

import Image from "next/image";
 
<Image src="/hero.jpg" alt="説明文" width={1200} height={600} priority sizes="100vw" />
  • next/image を使い、width/height(または fill + sizes)を指定して CLS を防ぐ。
  • ファーストビューの LCP 画像には priority を付ける。
  • 外部画像は images.remotePatterns で許可ドメインを限定する。

フォント

import { Noto_Sans_JP } from "next/font/google";
const notoSansJP = Noto_Sans_JP({ subsets: ["latin"], weight: ["400", "700"], display: "swap" });
  • next/font でセルフホスト化し、レイアウトシフトと外部リクエストを削減する。

JavaScript 削減

  • "use client" の範囲を最小化する(最大の効果がある)。
  • 重いクライアントコンポーネントは next/dynamic で遅延読み込み。
  • サードパーティスクリプトは next/script(strategy="lazyOnload" など)や @next/third-parties を使う。
  • @next/bundle-analyzer で定期的にバンドルサイズを確認する。

その他

  • Turbopack は Next.js 16 で開発・ビルドともにデフォルト。特別な理由がなければそのまま使う。
  • React Compiler を有効化すると useMemo / useCallback の手動最適化をほぼ不要にできる(reactCompiler: true)。
  • Core Web Vitals(LCP / INP / CLS)を useReportWebVitals や Vercel Speed Insights で計測する。

9. SEO とメタデータ

// 静的
export const metadata: Metadata = {
  title: { default: "サイト名", template: "%s | サイト名" },
  description: "サイトの説明",
};
 
// 動的
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const post = await getPost(id); // cache() でページ本体と重複排除される
  return {
    title: post.title,
    openGraph: { images: [post.ogImage] },
  };
}
  • app/sitemap.ts と app/robots.ts でサイトマップ・robots.txt を生成する。
  • OG 画像は opengraph-image.tsx(ImageResponse)で動的生成できる。
  • 構造化データ(JSON-LD)は <script type="application/ld+json"> で埋め込む。
  • metadataBase をルートレイアウトで設定し、相対 URL を正しく解決させる。

10. セキュリティ

  • 常に最新パッチを適用する。 Next.js は定期的なセキュリティリリースを行っている。Dependabot / Renovate で自動化する。
    • 例: CVE-2025-66478 は React Server Components のプロトコルにおけるリモートコード実行の脆弱性(CVSS 10.0)で、Next.js 16.0.4 で修正された。App Router を使っているだけで影響を受けうるため、セキュリティ告知は必ず追う。
  • 認証は自前で実装しない。 Auth.js、Better Auth、Clerk などの実績あるライブラリ・サービスを使う。セッション Cookie は HttpOnly・Secure・SameSite=Lax 以上にする。
  • 環境変数:
    • NEXT_PUBLIC_ 付きはクライアントに露出する。シークレットには絶対に付けない。
    • 起動時に Zod などで環境変数を検証する(@t3-oss/env-nextjs など)。
  • Server Actions / Route Handlers は毎回認証・認可・入力検証を行う。
  • Taint API(experimental_taintObjectReference / taintUniqueValue)で機密値のクライアント流出を防ぐことも検討する。
  • データ最小化: DB のレコードをそのまま Client Component に渡さず、必要なフィールドだけの DTO にする。
  • セキュリティヘッダー(CSP、X-Frame-Options、Referrer-Policy など)を next.config.ts の headers() または Proxy で設定する。
  • dangerouslySetInnerHTML を使う場合は必ずサニタイズ(DOMPurify など)。
  • 外部画像・リダイレクト先は許可リストで制限する(オープンリダイレクト対策)。

11. 型安全性とコード品質

  • TypeScript strict モードを有効にする。
  • Typed Routes(typedRoutes: true)で <Link href> の型チェックを行う。
  • next typegen / 自動生成される PageProps<"/posts/[id]"> などのヘルパー型で params を型安全にする。
  • next lint は Next.js 16 で削除されたため、ESLint(eslint-config-next)または Biome を直接実行する。
  • Prettier(または Biome)でフォーマットを統一し、CI で tsc --noEmit と Lint を実行する。

12. テスト

種類ツール例対象
ユニットVitest / Jest + Testing Libraryユーティリティ、Client Component、同期 Server Component
E2EPlaywrightasync Server Component、ページ遷移、フォーム送信
ビジュアルStorybook + ChromaticUI コンポーネント
  • async Server Component はユニットテストツールの対応が限定的なため、E2E でカバーするのが現実的。
  • データアクセス層・Server Action のロジックは関数として切り出し、ユニットテストしやすくする。

13. デプロイと運用

  • output: "standalone" で Docker イメージを軽量化できる(セルフホスト時)。

  • セルフホストで複数インスタンスを動かす場合は、共有キャッシュハンドラ(Redis など)を設定する。

  • Vercel 以外のプラットフォーム(OpenNext、Cloudflare、AWS Amplify など)は Build Adapters API 経由で proxy.ts や Cache Components を含むフル機能に対応している。採用前に利用するアダプターの対応状況を確認する。

  • メジャーバージョンのアップグレードは codemod を使う。

    npx @next/codemod@canary upgrade latest
  • instrumentation.ts で OpenTelemetry・Sentry などの監視を初期化する。

  • エラー監視(Sentry など)とログ基盤を本番前に整える。

  • プレビュー環境(PR ごとのデプロイ)でレビューする運用にする。

  • Node.js は 20.9 以上(LTS 推奨)。


14. アンチパターン集

アンチパターン問題代替策
ルートレイアウトやページ全体に "use client"JS が肥大化、Server Components の利点を失うインタラクティブ部分だけ切り出す
Server Component から自アプリの /api/* を fetch余計な往復、ビルド時に失敗することもDAL の関数を直接呼ぶ
useEffect で初期データを取得ウォーターフォール、SEO 不利、ちらつきServer Component で取得
認可を Proxy だけで実施バイパスされうるDAL・Server Action でも検証
Server Action で入力検証なし任意データの書き込みZod などで検証
ユーザー固有データを共有キャッシュ他ユーザーへの情報漏洩キャッシュしない / private スコープ
await を順番に並べる不要なウォーターフォールPromise.all / Suspense
<a> で内部リンクフルリロード、プリフェッチなし<Link>
<img> 直書き最適化なし、CLSnext/image
シークレットに NEXT_PUBLIC_クライアントに露出サーバー専用環境変数
redirect() を try/catch の中で呼ぶ例外が捕まり遷移しないtry/catch の外で呼ぶ
Webhook の署名を検証しないなりすましリクエストを処理してしまう署名検証してから処理
認可をレイアウトだけで実施ナビゲーション時に再実行されずすり抜けるDAL で毎回チェック

15. レビューチェックリスト

アーキテクチャ

  • "use client" は必要最小限の末端コンポーネントだけに付いている
  • サーバー専用モジュールに import "server-only" がある
  • ビジネスロジックが app/ から分離されている

データ・キャッシュ

  • 独立したデータ取得が並列化されている
  • 遅い部分が <Suspense> で分離されている
  • キャッシュ対象・期間・タグが意図通りに設計されている
  • ユーザー固有データが共有キャッシュに入っていない

セキュリティ

  • すべての Server Action / Route Handler で認証・認可・入力検証をしている
  • データアクセスが DAL を経由している(ページやレイアウトから DB を直接呼んでいない)
  • Webhook の署名検証と、外部公開 API のレート制限がある
  • クライアントに渡すデータが DTO で最小化されている
  • シークレットに NEXT_PUBLIC_ が付いていない
  • Next.js / React が最新のパッチバージョン

UX・パフォーマンス

  • loading.tsx / error.tsx / not-found.tsx が適切に配置されている
  • 画像は next/image、フォントは next/font
  • LCP 画像に priority が付いている
  • メタデータ(title / description / OG)が設定されている

品質

  • TypeScript strict、型エラーなし
  • Lint / フォーマットが CI で実行されている
  • 主要フローに E2E テストがある

参考リンク

0件のページ