Next.js プロダクション運用
Next.js 16 App Router アプリのコードに使用します。セキュリティ固有のルール(アクション内の認証、CSP、ヘッダー、taint)は nextjs-security に、コンテナのルールは docker-nextjs にあります。Claude Code のビルトイン next-best-practices スキルはファイル規則、メタデータ、画像・フォント最適化を詳細にカバーしています。このスキルでは、Nuvraxis での構築・出荷方法に固有のルールのみを追加します。
基準: Next 16.4、React 19.3、Turbopack(dev および build のデフォルト)、cacheComponents: true(partialPrefetching: true 付き)、output: "standalone"、typedRoutes: true。テンプレートの next.config.ts が信頼できる情報源です。
ルール
-
サーバーコンポーネントをデフォルトとし、
"use client"は状態、エフェクト、ブラウザAPI、またはイベントハンドラが必要な場合のみ使用する。 ディレクティブは、それが本当に必要な最小のリーフに配置します。ファイルが nuvui をインポートしているからといって追加してはいけません。nuvui のステートフルな部分は独自のディレクティブを持っています(nuvuiスキール、rules/server-components.mdを参照)。// 誤り: 1つのハンドラのためにページ全体がクライアントになる "use client"; export default function Page() { … <Select onValueChange={…} /> … } // 正しい: ページはサーバーに残り、ハンドラはリーフにある export default async function Page() { const people = await getPeople(); return <PeopleFilter people={people} />; // people-filter.tsx に "use client" がある }クライアントコンポーネントに渡すプロパティはシリアライズ可能でなければなりません。関数(Server Actions を除く)やクラスインスタンスは許可されません。
-
データを必要とするコンポーネント内で、サーバーサイドでフェッチする。
useEffectによるフェッチや、単に自分のページにデータを供給するためのルートハンドラは使用しません。リクエストごとの重複排除には React のcache()を使用し、リクエスト間のキャッシュにはルール3を使用します。 -
"use cache"+cacheLife+cacheTagでキャッシュする。それ以外はすべて動的とする。 キャッシュコンポーネントでは、明示的に指定しない限り何もキャッシュされず、fetchに暗黙のキャッシュはありません。import { cacheLife, cacheTag } from "next/cache"; export async function getInvoice(id: InvoiceId) { "use cache"; cacheLife("minutes"); cacheTag(`invoice:${id}`); return db.invoice.findUnique({ where: { id } }); }古いセグメント設定(
export const dynamic、revalidate、fetchCache)は使用しないでください。キャッシュコンポーネントではエラーになります。引数とクロージャでキャプチャされた値がキャッシュキーの一部となるため、リクエストオブジェクト全体ではなく ID を渡します。詳細、プロファイル、および"use cache: private"については、references/caching.md を参照してください。 -
Server Actions 内では
updateTagで、それ以外ではrevalidateTag(tag, "max")で無効化する。updateTag(tag)は「読み取り後書き込み」の整合性を提供し、Server Actions 内でのみ動作します。Webhook やルートハンドラからはrevalidateTag(tag, "max")(stale-while-revalidate)またはrevalidateTag(tag, { expire: 0 })を使用します。1引数のrevalidateTag(tag)は非推奨です。 -
リクエスト時にしか確定しないデータは
<Suspense>でラップする。そうしないとビルドが失敗する。cookies()、headers()、searchParams、connection()、およびキャッシュされていない I/O を Suspense 境界外で使用すると、「プリレンダリング中にキャッシュされていないデータまたはランタイムデータ」というエラーでプリレンダリングが停止します。スケルトンフォールバック付きの境界を設定するか、キャッシュするか、または(ルートレイアウトのみ。ルール14を参照)export const instant = falseでルートをブロッキングにマークします。params、searchParams、cookies()、headers()はすべて非同期です。awaitしてください。 -
すべての Server Action は、自身で入力の検証と認可のチェックを行う。 Server Action は、どのページに書かれていても公開 POST エンドポイントです。
FormDataはtypescript-production(valibot)のスキーマでパースし、認可し、ミューテーションし、updateTagします。期待される失敗には型付きの結果を返し、バグの場合のみスローします。完全なルールセットはnextjs-securityにあります。"use server"; export async function renameProject(_: State, form: FormData): Promise<State> { const session = await requireSession(); // 不在時は 401 const input = v.safeParse(RenameProject, Object.fromEntries(form)); if (!input.success) return { ok: false, errors: flatten(input.issues) }; await assertCanEdit(session, input.output.projectId); // 許可されていない場合は 403 await db.project.update(…); updateTag(`project:${input.output.projectId}`); return { ok: true }; } -
ルートハンドラは、他のクライアント(Webhook、モバイル、ヘルスチェック)向けであり、自分のページ向けではない。
app/api/配下に配置します。アクションと同様にボディを検証し、Response.json(...)を返し、ステータスコードを意図的に設定します。 -
すべてのアプリに動的で依存関係のない
GET /api/healthを設置する。 Docker のHEALTHCHECKと Kubernetes のプローブがこれを叩きます。データベースにアクセスせずに応答する必要があるため、遅い依存関係があってもすべての Pod が再起動されることはありません。await connection()により、静的ファイルとしてプリレンダリングされることを防ぎます。テンプレートのsrc/app/api/health/route.tsにあり、proxy.tsは/api/healthをマッチャーから除外します。 -
ルートレベル、および UX が異なるセグメントごとに
error.tsx、loading.tsx、not-found.tsxを配置する。error.tsxはクライアントコンポーネントです。instrumentation.tsのonRequestErrorでログ出力し、resetでリトライできるように表示します。エラーメッセージは絶対に表示しません。空の状態をレンダリングする代わりに、レコードが見つからない場合はnotFound()を呼び出します。 -
NEXT_PUBLIC_*はビルド時に埋め込まれ、それ以外はランタイムにサーバー上で読み込まれる。 1つのイメージが変更されずに main からプロダクションに昇格するため、NEXT_PUBLIC_の値はすべての環境で同じです。ビルド時の定数(アプリ名、すべての環境で同じパブリック Sentry DSN)のみをそこに配置します。環境ごとの設定(API URL、フィーチャーフラグ、シークレット)はサーバーサイドのprocess.env.Xであり、サーバーコンポーネントまたはアクション内で読み取り、必要に応じてプロパティとしてクライアントに渡します。環境変数はsrc/env.ts(valibot)で1回検証し、そこからインポートします。next.config.tsのenvキーは使用しないでください。クライアントバンドルにインライン化されます。 -
サーバー専用モジュールには
import "server-only"を記述する。 シークレットを読み取るもの、データベースにアクセスするもの、認証情報を持つ SDK をインポートするものなど。クライアントがこれをインポートすると、情報漏洩ではなくビルドエラーになります。 -
after()はレスポンスを遅らせるべきでない処理(監査ログ、分析、キャッシュウォームアップ)に使用する。afterを呼び出す前にcookies()/headers()を読み取り、after内では読み取りません。同じプロセス内で実行されるため、Pod は SIGTERM の猶予期間を必要とします(チャートでterminationGracePeriodSecondsを設定)。 -
src/instrumentation.tsでregisterOTel+onRequestErrorによりインストルメンテーションを行う。@vercel/otelによる OpenTelemetry、エクスポーターはランタイムの標準OTEL_EXPORTER_OTLP_*環境変数で設定します。references/observability.md を参照してください。 -
output: "standalone"を維持し、ワークスペースの外部からインポートしない。 Docker ランナーは.next/standalone、.next/static、publicのみをコピーします。モノレポでは、server.jsは.next/standalone/apps/<app>/server.jsに配置されます。ワークスペースルートはpnpm-lock.yamlから検出されるため、ビルドが警告しない限りoutputFileTracingRootやturbopack.rootは設定しないでください。セルフホスティングの詳細(マルチレプリカ、Server Action キー、deploymentId)については、references/self-hosting.md を参照してください。 -
ルートレイアウトは CSP nonce を読み取るため、アプリはリクエストごとにレンダリングされる。 テンプレートの
proxy.tsは nonce ベースの CSP を設定し、src/app/layout.tsxにはexport const instant = falseがあります。これは意図的なトレードオフです。nonce とプリレンダリングされた静的シェルは共存できず、ログイン済みのビジネスアプリでは厳格な CSP が優先されます。"use cache"によるデータキャッシュは引き続き機能します。nextjs-securityのルール4を読まずにこれらのいずれかを削除しないでください。 -
スタイル: グローバル BEM SCSS、コンポーネントがインポートする。 Turbopack は
sassパッケージで Sass をコンパイルします。sassOptions.loadPaths: ["src/styles"]により、任意の深さから@use "abstracts"が機能します。~インポートやsassOptions.functionsは使用しません。ルールはscss-bemにあります。 -
画像は
next/image、フォントはnext/fontを使用する。sharpは最適化のために Next に同梱されています。ランナーイメージに追加のパッケージは必要ありません。リモート画像には、正確なホスト名を指定したimages.remotePatternsが必要です。 -
pnpm next analyzeでバンドルサイズをチェックする(Turbopack のアナライザー。@next/bundle-analyzerは webpack 専用)。重いクライアントウィジェット(nuvui エディター、マップ、スクロールしないと見えないチャート)はnext/dynamicで遅延読み込みします。 -
型付きルートと生成された型を使用する。
typedRoutes: trueは<Link href>をチェックします。PageProps<"/invoices/[id]">とLayoutPropsは、next typegen(next dev/next buildで自動実行)の後にグローバルになります。paramsの型を手書きしないでください。 -
next lintは使用しない。 16 で削除されました。Biome がリンターとして機能します(biomeを参照)。TypeScript はパッケージごとにtsc --noEmitでチェックします。
確認
pnpm typecheck
pnpm lint
pnpm build # turbo run build: プリレンダリングエラー、型エラー、Sass エラーで失敗
pnpm --filter <app> start & # その後:
curl -fsS localhost:3000/api/health
ソース
- https://nextjs.org/docs/app/guides/upgrading/version-16
- https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents
- https://nextjs.org/docs/app/guides/migrating-to-cache-components
- https://nextjs.org/docs/app/api-reference/directives/use-cache
- https://nextjs.org/docs/app/api-reference/functions/revalidateTag
- https://nextjs.org/docs/app/api-reference/functions/updateTag
- https://nextjs.org/docs/messages/blocking-prerender-dynamic
- https://nextjs.org/docs/app/api-reference/functions/after
- https://nextjs.org/docs/app/guides/open-telemetry
- https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- https://nextjs.org/docs/app/guides/self-hosting
- https://nextjs.org/docs/app/guides/environment-variables
- https://nextjs.org/docs/app/guides/sass
- https://nextjs.org/docs/app/guides/package-bundling
- https://nextjs.org/docs/app/api-reference/file-conventions/proxy