Next.js getServerSidePropsの業務実装パターン|実務で使える認証・データ取得・エラーハンドリング
\n\n
1. getServerSidePropsとは|簡易的な解説
\n\n
Next.jsのgetServerSidePropsは、ページコンポーネントをレンダリングする前にサーバー側で実行される関数です。リクエストごとに呼び出されるため、常に最新のデータを取得できるサーバーサイドレンダリング(SSR)を実現します。
\n\n
クライアントサイドで実行されるgetStaticPropsと異なり、getServerSidePropsはページがリクエストされるたびに実行されます。つまり、認証が必要なページ、頻繁に更新されるデータ、ユーザー固有の情報が必要なシーンに最適です。
\n\n
基本的な構文は以下の通りです:
\n\n
export async function getServerSideProps(context) {\n return {\n props: {}\n }\n}\n
\n\n
関数の引数contextには、リクエストオブジェクト、レスポンスオブジェクト、クエリパラメータなどが含まれます。これらを活用することで、業務で必要な多くの要件に対応できます。
\n
\n\n
2. 業務でのユースケース
\n\n
実務では、以下のようなシーンでgetServerSidePropsが活躍します:
\n\n
- \n
- ユーザー認証が必要なページ:セッション情報をチェックし、認証されていなければログインページにリダイレクト
- 会員情報の表示ページ:ユーザーIDごとに異なるデータを取得する必要がある場合
- 管理画面のダッシュボード:アクセス権限を確認してからデータを返す
- 外部APIからのリアルタイムデータ取得:常に最新のデータを表示する必要があるページ
- 動的ルートのページ生成:URLパラメータに基づいて異なるデータを取得
- キャッシュ制御が必要なデータ:Cache-Controlヘッダーをサーバーサイドで制御する場合
\n
\n
\n
\n
\n
\n
\n\n
これらのユースケースはeコマースサイト、SaaS、ポータルサイト、会員制メディアなど、様々な業務アプリケーションで日常的に出現します。
\n
\n\n
3. 実装コード|業務でそのまま使えるパターン
\n\n
3-1. 認証チェック付きのページ実装
\n\n
最も一般的なパターンが、セッション認証を確認してからデータを表示するページです。以下は、実務で即座に使える実装例です:
\n\n
// pages/dashboard.tsx\nimport { GetServerSideProps } from 'next';\nimport { getSession } from 'next-auth/react';\nimport { Session } from 'next-auth';\n\ninterface DashboardProps {\n userName: string;\n userEmail: string;\n dashboardData: {\n totalSales: number;\n activeUsers: number;\n lastUpdated: string;\n };\n}\n\nfunction DashboardPage({ userName, userEmail, dashboardData }: DashboardProps) {\n return (\n \n ダッシュボード
\n ようこそ、{userName}様
\n メール:{userEmail}
\n \n 売上統計
\n 総売上:¥{dashboardData.totalSales.toLocaleString()}
\n アクティブユーザー:{dashboardData.activeUsers}
\n 更新時刻:{dashboardData.lastUpdated}
\n \n \n );\n}\n\nexport const getServerSideProps: GetServerSideProps = async (context) => {\n const session = await getSession({ req: context.req });\n\n // 認証されていない場合はログインページへリダイレクト\n if (!session) {\n return {\n redirect: {\n destination: '/login',\n permanent: false,\n },\n };\n }\n\n try {\n // 認証済みユーザーのデータを取得\n const dashboardResponse = await fetch(\n `${process.env.NEXT_PUBLIC_API_URL}/api/dashboard`,\n {\n headers: {\n Authorization: `Bearer ${session.user.accessToken}`,\n },\n }\n );\n\n if (!dashboardResponse.ok) {\n throw new Error('ダッシュボードデータの取得に失敗しました');\n }\n\n const dashboardData = await dashboardResponse.json();\n\n return {\n props: {\n userName: session.user.name,\n userEmail: session.user.email,\n dashboardData,\n },\n // ページキャッシュを10秒に設定(リアルタイムデータのため短く設定)\n revalidate: 10,\n };\n } catch (error) {\n console.error('getServerSideProps error:', error);\n\n // エラー時は一度だけリトライするため、notFoundではなくコンポーネント側でエラーハンドリング\n return {\n props: {\n userName: session.user.name,\n userEmail: session.user.email,\n dashboardData: {\n totalSales: 0,\n activeUsers: 0,\n lastUpdated: new Date().toISOString(),\n },\n },\n revalidate: 5,\n };\n }\n};\n\nexport default DashboardPage;\n
\n\n
3-2. 動的ルートでのデータ取得
\n\n
ユーザーIDや商品IDなどのパラメータに基づいてデータを取得する場合、以下のように実装します:
\n\n
// pages/users/[userId].tsx\nimport { GetServerSideProps } from 'next';\n\ninterface UserProfileProps {\n userId: string;\n userName: string;\n userEmail: string;\n joinDate: string;\n isPremium: boolean;\n profileImage: string;\n}\n\nfunction UserProfilePage(props: UserProfileProps) {\n const { userId, userName, userEmail, joinDate, isPremium, profileImage } = props;\n\n return (\n \n
\n {userName}
\n ユーザーID:{userId}
\n メール:{userEmail}
\n 登録日:{new Date(joinDate).toLocaleDateString('ja-JP')}
\n {isPremium && プレミアム会員}\n \n );\n}\n\nexport const getServerSideProps: GetServerSideProps = async (context) => {\n const { userId } = context.params as { userId: string };\n\n // ユーザーIDの妥当性チェック(数値のみ許可)\n if (!/^\\d+$/.test(userId)) {\n return {\n notFound: true,\n };\n }\n\n try {\n const userResponse = await fetch(\n `${process.env.NEXT_PUBLIC_API_URL}/api/users/${userId}`,\n {\n headers: {\n 'User-Agent': context.req.headers['user-agent'] || '',\n },\n // タイムアウト設定(3秒で切断)\n signal: AbortSignal.timeout(3000),\n }\n );\n\n if (userResponse.status === 404) {\n return {\n notFound: true,\n };\n }\n\n if (!userResponse.ok) {\n throw new Error(`API error: ${userResponse.status}`);\n }\n\n const userData = await userResponse.json();\n\n return {\n props: {\n userId,\n userName: userData.name,\n userEmail: userData.email,\n joinDate: userData.createdAt,\n isPremium: userData.isPremium,\n profileImage: userData.profileImage,\n },\n // ユーザー情報は比較的安定しているため、キャッシュを60秒に設定\n revalidate: 60,\n };\n } catch (error) {\n console.error(`Failed to fetch user ${userId}:`, error);\n\n return {\n notFound: true,\n };\n }\n};\n\nexport default UserProfilePage;\n
\n\n
3-3. 権限チェック付きの管理画面
\n\n
管理権限をチェックして、必要に応じてアクセスを制限するパターンです:
\n\n
// pages/admin/users.tsx\nimport { GetServerSideProps } from 'next';\nimport { getSession } from 'next-auth/react';\nimport { Session } from 'next-auth';\n\ninterface AdminUsersPageProps {\n users: Array<{\n id: string;\n name: string;\n email: string;\n status: 'active' | 'inactive' | 'suspended';\n role: 'user' | 'moderator' | 'admin';\n createdAt: string;\n }>;\n totalCount: number;\n pageNumber: number;\n}\n\nfunction AdminUsersPage({ users, totalCount, pageNumber }: AdminUsersPageProps) {\n return (\n \n ユーザー管理
\n 合計:{totalCount}名
\n \n \n \n ID \n 名前 \n メール \n ステータス \n 権限 \n 登録日 \n \n \n \n {users.map((user) => (\n \n {user.id} \n {user.name} \n {user.email} \n {user.status} \n {user.role} \n {new Date(user.createdAt).toLocaleDateString('ja-JP')} \n \n ))}\n \n
\n ページ {pageNumber}
\n \n );\n}\n\nexport const getServerSideProps: GetServerSideProps = async (context) => {\n const session = await getSession({ req: context.req });\n const pageNumber = parseInt(context.query.page as string) || 1;\n\n // 認証チェック\n if (!session) {\n return {\n redirect: {\n destination: '/login',\n permanent: false,\n },\n };\n }\n\n // 管理者権限チェック\n if (session.user.role !== 'admin') {\n return {\n redirect: {\n destination: '/',\n permanent: false,\n },\n };\n }\n\n try {\n // ページネーション処理\n const pageSize = 20;\n const skip = (pageNumber - 1) * pageSize;\n\n const usersResponse = await fetch(\n `${process.env.NEXT_PUBLIC_API_URL}/api/admin/users?skip=${skip}&limit=${pageSize}`,\n {\n headers: {\n Authorization: `Bearer ${session.user.accessToken}`,\n 'X-Admin-Request': 'true',\n },\n signal: AbortSignal.timeout(5000),\n }\n );\n\n if (!usersResponse.ok) {\n throw new Error('ユーザーリスト取得失敗');\n }\n\n const data = await usersResponse.json();\n\n return {\n props: {\n users: data.users,\n totalCount: data.totalCount,\n pageNumber,\n },\n // 管理画面は頻繁に更新されるため、短いキャッシュ時間を設定\n revalidate: 5,\n };\n } catch (error) {\n console.error('Admin users fetch error:', error);\n\n return {\n props: {\n users: [],\n totalCount: 0,\n pageNumber,\n },\n revalidate: 2,\n };\n }\n};\n\nexport default AdminUsersPage;\n
\n
\n\n
4. よくある応用パターン
\n\n
4-1. 複数のAPIを並列実行する
\n\n
業務では、複数のデータソースから情報を取得することが多いです。Promise.allを使って並列実行し、パフォーマンスを最適化します:
\n\n
export const getServerSideProps: GetServerSideProps = async (context) => {\n const session = await getSession({ req: context.req });\n\n if (!session) {\n return { redirect: { destination: '/login', permanent: false } };\n }\n\n try {\n // 複数のAPIリクエストを並列実行\n const [profileRes, ordersRes, notificationsRes] = await Promise.all([\n fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/profile`, {\n headers: { Authorization: `Bearer ${session.user.accessToken}` },\n }),\n fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/orders`, {\n headers: { Authorization: `Bearer ${session.user.accessToken}` },\n }),\n fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/notifications`, {\n headers: { Authorization: `Bearer ${session.user.accessToken}` },\n }),\n ]);\n\n if (!profileRes.ok || !ordersRes.ok || !notificationsRes.ok) {\n throw new Error('一部のAPIリクエストが失敗しました');\n }\n\n const [profile, orders, notifications] = await Promise.all([\n profileRes.json(),\n ordersRes.json(),\n notificationsRes.json(),\n ]);\n\n return {\n props: {\n profile,\n orders,\n notifications,\n },\n revalidate: 10,\n };\n } catch (error) {\n console.error('Parallel API calls failed:', error);\n return {\n notFound: true,\n };\n }\n};\n
\n\n
4-2. リダイレクトとデータ取得の組み合わせ
\n\n
条件に基づいて異なるページにリダイレクトしたり、データを加工したりする場合:
\n\n
export const getServerSideProps: GetServerSideProps = async (context) => {\n const { orderId } = context.params as { orderId: string };\n const session = await getSession({ req: context.req });\n\n if (!session) {\n return { redirect: { destination: '/login', permanent: false } };\n }\n\n try {\n const orderResponse = await fetch(\n `${process.env.NEXT_PUBLIC_API_URL}/api/orders/${orderId}`,\n {\n headers: { Authorization: `Bearer ${session.user.accessToken}` },\n }\n );\n\n if (!orderResponse.ok) {\n return { notFound: true };\n }\n\n const order = await orderResponse.json();\n\n // ビジネスロジック:特定の条件でリダイレクト\n if (order.status === 'cancelled') {\n return {\n redirect: {\n destination: '/orders',\n permanent: false,\n },\n };\n }\n\n // データの変換・加工\n const processedOrder = {\n ...order,\n formattedPrice: `¥${order.totalPrice.toLocaleString()}`,\n statusLabel: getStatusLabel(order.status),\n estimatedDelivery: new Date(order.createdAt).getTime() + 7 * 24 * 60 * 60 * 1000,\n };\n\n return {\n props: {\n order: processedOrder,\n },\n revalidate: 30,\n };\n } catch (error) {\n console.error('Order fetch failed:', error);\n return {\n notFound: true,\n };\n }\n};\n\nfunction getStatusLabel(status: string): string {\n const labels: Record = {\n pending: '処理中',\n confirmed: '確定',\n shipped: '発送済み',\n delivered: '配達済み',\n cancelled: 'キャンセル',\n };\n return labels[status] || status;\n}\n
\n\n
4-3. クエリパラメータの検証と処理
\n\n
フィルタリングやソートなど、クエリパラメータに基づく動的な処理:
\n\n
interface ProductListProps {\n products: Array<{ id: string; name: string; price: number; category: string }>;\n currentPage: number;\n totalPages: number;\n sortBy: string;\n category: string;\n}\n\nexport const getServerSideProps: GetServerSideProps = async (context) => {\n // クエリパラメータの取得と妥当性チェック\n const page = Math.max(1, parseInt(context.query.page as string) || 1);\n const sortBy = (context.query.sort as string) || 'popularity';\n const category = (context.query.category as string) || '';\n const pageSize = 20;\n\n // 許可されたソート順のみを受け付ける(インジェクション対策)\n const allowedSorts = ['popularity', 'price_asc', 'price_desc', 'newest'];\n const validSort = allowedSorts.includes(sortBy) ? sortBy : 'popularity';\n\n // 許可されたカテゴリのみ処理\n const allowedCategories = ['electronics', 'clothing', 'books', 'food'];\n const validCategory = category && allowedCategories.includes(category) ? category : '';\n\n try {\n const queryParams = new URLSearchParams();\n queryParams.append('page', String(page));\n queryParams.append('limit', String(pageSize));\n queryParams.append('sort', validSort);\n if (validCategory) {\n queryParams.append('category', validCategory);\n }\n\n const productsResponse = await fetch(\n `${process.env.NEXT_PUBLIC_API_URL}/api/products?${queryParams.toString()}`,\n {\n signal: AbortSignal.timeout(5000),\n }\n );\n\n if (!productsResponse.ok) {\n throw new Error('商品リスト取得失敗');\n }\n\n const data = await productsResponse.json();\n\n return {\n props: {\n products: data.products,\n currentPage: page,\n totalPages: Math.ceil(data.totalCount / pageSize),\n sortBy: validSort,\n category: validCategory,\n },\n revalidate: 60,\n };\n } catch (error) {\n console.error('Product list fetch failed:', error);\n return {\n props: {\n products: [],\n currentPage: page,\n totalPages: 0,\n sortBy: validSort,\n category: validCategory,\n },\n revalidate: 10,\n };\n }\n};\n
\n
\n\n
5. 注意点とベストプラクティス
\n\n
5-1. パフォーマンスとタイムアウト
\n\n
getServerSidePropsはリクエストのたびに実行されるため、パフォーマンスが重要です。以下の点に注意してください:
\n\n
- \n
- タイムアウト設定:APIリクエストに必ずタイムアウトを設定します。Vercelの本番環境では10秒以上かかると自動的にタイムアウトします
- キャッシュの活用:
revalidateプロパティでISR(Incremental Static Regeneration)を使い、不必要なリクエストを削減します - 不要なAPI呼び出しの削減:データベースクエリを最適化し、N+1問題を避けます
\n
\n
\n
\n\n
5-2. セキュリティ対策
\n\n
サーバーサイドで実行されるため、認証情報を安全に扱う必要があります:
\n\n
// ❌ 悪い例:秘密鍵がクライアントに公開される\nexport const getServerSideProps = async () => {\n const response = await fetch(url, {\n headers: {\n 'X-API-Key': process.env.NEXT_PUBLIC_API_KEY, // 危険!\n },\n });\n};\n\n// ✅ 良い例:秘密鍵はサーバーサイドのみで使用\nexport const getServerSideProps = async () => {\n const response = await fetch(url, {\n headers: {\n 'X-API-Key': process.env.API_SECRET_KEY, // サーバーサイドのみ\n },\n });\n};\n
\n\n
5-3. エラーハンドリング
\n\n
実務では、APIが必ず成功するとは限りません。適切なエラーハンドリングが重要です:
\n\n
export const getServerSideProps: GetServerSideProps = async (context) => {\n try {\n const response = await fetch(apiUrl, {\n signal: AbortSignal.timeout(3000),\n });\n\n if (response.status === 503) {\n // サービス一時停止時は、キャッシュを少し長く設定\n return {\n props: { fallbackData: true },\n revalidate: 60,\n };\n }\n\n if (!response.ok) {\n throw new Error(`API error: ${response.status}`);\n }\n\n const data = await response.json();\n return { props: { data }, revalidate: 10 };\n } catch (error) {\n // ネットワークエラーやタイムアウトの場合\n if (error instanceof TypeError) {\n console.error('Network error:', error);\n return {\n props: { fallbackData: true },\n revalidate: 5,\n };\n }\n\n // 予期しないエラー\n console.error('Unexpected error:', error);\n return {\n notFound: true,\n };\n }\n};\n
\n\n
5-4. Cookie とセッション情報の扱い
\n\n
認証情報を安全に取得し、APIリクエストに含めます:
\n\n
export const getServerSideProps: GetServerSideProps = async (context) => {\n const { req, res } = context;\n\n // next-authを使用している場合\n const session = await getSession({ req });\n\n // または、手動でCookieから取得\n const sessionToken = req.cookies.sessionToken;\n\n if (!sessionToken) {\n return {\n redirect: {\n destination: '/login',\n permanent: false,\n },\n };\n }\n\n try {\n const response = await fetch(apiUrl, {\n headers: {\n Cookie: `sessionToken=${sessionToken}`,\n },\n });\n\n const data = await response.json();\n\n return {\n props: { data },\n revalidate: 10,\n };\n } catch (error) {\n return { notFound: true };\n }\n};\n
\n\n
5-5. スケーラビリティの考慮
\n\n
アクセスが増加した場合のことを考慮します:
\n\n
- \n
- キャッシング戦略:Redisなどのキャッシュレイヤーを導入し、同じリクエストへの重複アクセスを削減
- CDN活用:静的資産をCDNで配信し、レスポンス時間を短縮
- データベース最適化:インデックスの作成、クエリの最適化
- ISRの活用:可能な限り
revalidateを設定し、サーバーサイドレンダリングの回数を削減
\n
\n
\n
\n
\n
\n\n
6. 実務での落とし穴と対策
\n\n
6-1. 過度なAPI呼び出し
\n\n
getServerSidePropsはリクエストごとに実行されるため、不必要なAPI呼び出しは避けるべきです:
\n\n

