nextjs-app-router について
Next.js App Router(v13+)開発向けのリファレンススキルです。ルーティング、コンポーネント、データ処理、本番運用の指針を開発者に提供します。
主な機能
- 01App Routerのファイル規約
- 02サーバー/クライアントコンポーネントの指針
- 03データ取得とキャッシュのパターン
- 04変更処理のためのServer Actions
- 05セキュリティとエラー処理の指針
- 06ビルドとパフォーマンスのチェックリスト
活用シーン
- 01ルートに使うコンポーネントの選択
- 02取得データのキャッシュ動作の計画
- 03本番環境のセキュリティ設定
- 04App Routerの一般的な問題の診断
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill nextjs-app-routerインストール後、Claude Code で/nextjs-app-routerを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
advanced.mdquick-ref/data-fetching.mdfile-conventions.md詳細
Next.js App Router コア
完全なリファレンス:WebSocket の統合、Socket.IO、Server-Sent Events、TanStack Query によるリアルタイム同期、Vercel/Pusher のパターンについては、advanced.md を参照してください。
詳細な知識:包括的なドキュメントを確認するには、technology に
nextjsを指定してmcp__documentation__fetch_docsを使用してください。
ファイル規約
| ファイル | 目的 |
|---|---|
page.tsx | ルートの UI(ルートに必須) |
layout.tsx | 共有レイアウト。状態を保持します |
loading.tsx | 読み込み中の UI(Suspense) |
error.tsx | エラーバウンダリー |
not-found.tsx | 404 ページ |
route.ts | API エンドポイント |
コンポーネントの種類
サーバーコンポーネント(デフォルト)
// No 'use client' - runs on server
async function Page() {
const data = await db.query(...); // Direct DB access
return <div>{data.name}</div>;
}
クライアントコンポーネント
'use client'
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
データ取得
// Server Component
async function Page() {
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache', // Static (default)
// cache: 'no-store', // Dynamic
// next: { revalidate: 60 } // ISR
});
return <Component data={await data.json()} />;
}
サーバーアクション
'use server'
export async function createItem(formData: FormData) {
await db.items.create({ name: formData.get('name') });
revalidatePath('/items');
}
判断基準
| シナリオ | 使用するもの |
|---|---|
| インタラクティブな UI | 'use client' |
| データ取得 | サーバーコンポーネント |
| フォームの変更処理 | サーバーアクション |
| 共有状態 | クライアントコンポーネント |
本番環境への準備
セキュリティ設定
// next.config.js - Security headers
const securityHeaders = [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
{
key: 'Content-Security-Policy',
value: `default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';`,
},
];
module.exports = {
async headers() {
return [{ source: '/:path*', headers: securityHeaders }];
},
};
// Server-side secrets (never exposed to client)
// Use server components or API routes
async function SecureComponent() {
const apiKey = process.env.API_SECRET_KEY; // Server only
const data = await fetch(url, { headers: { Authorization: apiKey } });
return <div>{/* render data */}</div>;
}
// Environment variables
// NEXT_PUBLIC_* = exposed to client (careful!)
// Other vars = server-side only
エラー処理
// app/error.tsx - Error boundary
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log to error reporting service
captureException(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/global-error.tsx - Root error boundary
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</body>
</html>
);
}
パフォーマンスの最適化
// Dynamic imports for client components
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <Skeleton />,
ssr: false, // Client-only if needed
});
// Image optimization
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // For LCP images
placeholder="blur"
blurDataURL={blurHash}
/>
// Font optimization
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });
キャッシュ戦略
// Static data (cached indefinitely)
async function StaticPage() {
const data = await fetch(url, { cache: 'force-cache' });
}
// Dynamic data (never cached)
async function DynamicPage() {
const data = await fetch(url, { cache: 'no-store' });
}
// ISR (revalidate every 60 seconds)
async function ISRPage() {
const data = await fetch(url, { next: { revalidate: 60 } });
}
// On-demand revalidation
import { revalidatePath, revalidateTag } from 'next/cache';
async function updateData() {
'use server';
await db.update(...);
revalidatePath('/posts'); // Revalidate specific path
revalidateTag('posts'); // Revalidate by tag
}
ミドルウェアのセキュリティ
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Rate limiting header for upstream proxy
const response = NextResponse.next();
// Auth check
const token = request.cookies.get('session');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// CORS for API routes
if (request.nextUrl.pathname.startsWith('/api')) {
response.headers.set('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGIN!);
}
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
監視メトリクス
| メトリクス | アラートしきい値 |
|---|---|
| Largest Contentful Paint(LCP) | > 2.5s |
| First Input Delay(FID) | > 100ms |
| Cumulative Layout Shift(CLS) | > 0.1 |
| Time to First Byte(TTFB) | > 800ms |
| サーバーコンポーネントのレンダー時間 | > 200ms |
| ビルドサイズの増加 | > 10% |
ビルドとデプロイ
// next.config.js - Production optimizations
module.exports = {
output: 'standalone', // For Docker deployments
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.example.com' },
],
},
experimental: {
// Enable if using PPR
ppr: true,
},
};
# Production build with analysis
ANALYZE=true npm run build
# Check bundle size
npx @next/bundle-analyzer
チェックリスト
- セキュリティヘッダーを設定する
- CSP ポリシーを定義する
- NEXT_PUBLIC_* 変数にシークレットを含めない
- ルートレベルにエラーバウンダリーを設置する
- グローバルエラーバウンダリーを設置する
- next/image で画像を最適化する
- next/font でフォントを最適化する
- 大きなコンポーネントには動的インポートを使う
- ルートごとにキャッシュ戦略を設定する
- 認証/レート制限にミドルウェアを使う
- Core Web Vitals を監視する
- コンテナー化にはスタンドアロン出力を使う
このスキルを使用しないケース
このスキルは Next.js App Router(v13+)向けです。次の場合は使用しないでください。
- Next.js を使わない React:代わりに
frontend-reactスキルを使用してください - Next.js Pages Router(v12 以下):移行方法については KB を参照してください
- Nuxt.js(Vue メタフレームワーク):代わりに
nuxt3スキルを使用してください - Remix(React メタフレームワーク):代わりに
remixスキルを使用してください - SvelteKit:代わりに
sveltekitスキルを使用してください - Astro:代わりに
astroスキルを使用してください - API 専用バックエンド:代わりに
nestjsまたはfastapiを検討してください
アンチパターン
| アンチパターン | 問題である理由 | 正しい方法 |
|---|---|---|
| あらゆる場所で 'use client' を使う | サーバーコンポーネントの利点が失われ、バンドルサイズが増加します | インタラクティブなコンポーネントにのみ 'use client' を使用します |
| クライアントコンポーネントでデータを取得する | ウォーターフォールが発生し、SSR がなく、SEO に悪影響があります | サーバーコンポーネントで取得するか、サーバーアクションを使用します |
| loading.tsx を使わない | データ取得中の UX が低下します | ルートレベルの読み込み状態に loading.tsx を作成します |
| NEXT_PUBLIC_* にシークレットを含める | クライアントに公開され、セキュリティリスクになります | サーバー専用の環境変数またはサーバーコンポーネントを使用します |
| キャッシュ戦略を無視する | パフォーマンスが低下するか、古いデータが使われます | cache: 'force-cache'、'no-store'、または revalidate を明示的に設定します |
| エラーバウンダリーがない | 捕捉されないエラーによりアプリ全体がクラッシュします | ルートレベルとルート直下に error.tsx を追加します |
| cache オプションなしで fetch() を使う | キャッシュの動作が予測できません | 必ず cache または revalidate 戦略を指定します |
| データ取得に useEffect を使う | クライアント側のみで実行され、SSR がありません | async/await を使ったサーバーコンポーネントを使用します |
すぐに解決できるトラブルシューティング
| 問題 | 考えられる原因 | 解決策 |
|---|---|---|
| 「Server Component では useState を使用できません」 | 'use client' ディレクティブがない | ファイルの先頭に 'use client' を追加します |
| ミューテーション後にデータが更新されない | キャッシュが再検証されていない | Server Action で revalidatePath() または revalidateTag() を使用します |
| ハイドレーションの不一致エラー | サーバーとクライアントでレンダリング結果が異なる | データに一貫性があることを確認し、Date/ランダム値を確認します |
| コンポーネント内で「cookies にアクセスできません」と表示される | Cookies は Server Components/Actions でのみ使用可能 | ロジックを Server Component または API ルートに移動します |
| 「Dynamic server usage」でビルドが失敗する | 静的ルートで動的 API を使用している | export const dynamic = 'force-dynamic' を追加します |
| 画像が最適化されない | next/image を使用していない | |
| ページの読み込みが遅い | クライアントバンドルが大きい | 動的インポートを使用し、バンドルアナライザーを確認します |
| API ルートで CORS エラーが発生する | route.ts にヘッダーがない | ルートハンドラーに CORS ヘッダーを追加します |
リファレンスドキュメント
コメント
Markdown 対応