TOOLS BOX/ガイド/型安全な Session 管理パターン
Concept

型安全な Session 管理パターン

getSession() と requireSession() を分けて設計することで、型安全かつ責務を分離した Session 管理を実現するパターン。null を返す汎用関数と、未認証時に redirect する専用ラッパーの役割を整理する。

nextjssessionauthenticationtypescriptgetSessionrequireSessionreact-cacherole

どういう場面で使うか

  • ·getSession(): ページによって表示を出し分けたい(ログイン状態に依存するが、未認証でもアクセスできる)
  • ·requireSession(): そのページは必ずログインが必要(未認証なら redirect する)
  • ·requireRole(): 特定ロールのみアクセスできるページ

注意点 / Pitfalls

  • ·getSession() に redirect を内包させると、未認証でも表示できるページで使えなくなる
  • ·同一リクエスト内で getSession() を複数回呼ぶと DB クエリが重複する。React cache() でメモ化する
  • ·Session 型を any にすると、role チェックが型安全に書けなくなる
  • ·requireSession() の戻り値は Session(null でない)なので、呼び出し後に null チェックは不要

補足

getSession() と requireSession() を分けることで「認証状態に応じた表示切り替え」と「認証必須ページ」の両方を型安全に扱える。React cache() を使うと同一リクエスト内での重複クエリを防げる。

設計の核心:2 つの関数を分ける

Session 取得関数は用途によって 2 種類に分けます。

関数戻り値未認証時向いている場面
getSession()Session | nullnull を返す認証状態で表示を出し分けたいページ
requireSession()Session(non-null)redirect("/login")そのページにはログインが必須

getSession()redirect を内包させると、未認証でもアクセスできるページで使えなくなります。null を返す汎用関数redirect するラッパーを分けることが設計の起点です。

Session 型の定義

// lib/session.ts
export type Session = {
  userId: string;
  email: string;
  role: "user" | "admin";
  expiresAt: Date;
};

ロールは Union 型にすることで、後述の requireRole() で型安全なチェックができます。

getSession() — null を返す汎用関数

// lib/session.ts
import { cache } from "react";
import { cookies } from "next/headers";

// React cache() でリクエスト単位にメモ化(DB クエリの重複を防ぐ)
export const getSession = cache(async (): Promise<Session | null> => {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("session_id")?.value;
  if (!sessionId) return null;

  const session = await db.session.findUnique({
    where: { id: sessionId },
    select: { userId: true, email: true, role: true, expiresAt: true },
  });

  // 期限切れチェック
  if (!session || session.expiresAt < new Date()) return null;

  return session;
});

reactcache() を使う理由: Server Component は同一リクエスト内で複数のコンポーネントから getSession() を呼ぶことがあります。cache() でメモ化すると、リクエスト単位で 1 回だけ DB クエリが実行されます。

requireSession() — 認証必須ラッパー

// lib/session.ts
import { redirect } from "next/navigation";

export async function requireSession(): Promise<Session> {
  const session = await getSession();
  if (!session) {
    redirect("/login");
  }
  return session; // TypeScript: Session (null でない)
}

呼び出し側は null チェック不要になります。

// app/dashboard/page.tsx
export default async function DashboardPage() {
  const session = await requireSession(); // 未認証なら redirect される
  // session は Session 型(null でない)
  return <Dashboard userId={session.userId} />;
}

requireRole() — ロールチェックラッパー

権限が必要なページ向けのラッパーです。

// lib/session.ts
export async function requireRole(role: Session["role"]): Promise<Session> {
  const session = await requireSession();
  if (session.role !== role) {
    notFound(); // 存在を隠したい場合
    // または redirect("/forbidden") で権限不足を明示
  }
  return session;
}
// app/admin/page.tsx
export default async function AdminPage() {
  const session = await requireRole("admin"); // admin 以外は notFound
  return <AdminDashboard session={session} />;
}

認証状態で表示を出し分けるパターン

// app/page.tsx — ログイン状態で表示が変わるトップページ
export default async function TopPage() {
  const session = await getSession(); // null でもエラーにならない

  return (
    <main>
      <h1>TOOLS BOX</h1>
      {session ? (
        <p>{session.email} でログイン中</p>
      ) : (
        <a href="/login">ログイン</a>
      )}
    </main>
  );
}

責務分離のまとめ

getSession()
  ├── null を返す(redirect しない)
  ├── React cache() でメモ化
  └── 使う場面: 表示の出し分け、optional な認証

requireSession()
  ├── getSession() を呼んで null なら redirect("/login")
  ├── 戻り値は Session(TypeScript が non-null と認識)
  └── 使う場面: ログイン必須ページ

requireRole(role)
  ├── requireSession() を呼んでロールチェック
  ├── 不一致なら notFound() または redirect("/forbidden")
  └── 使う場面: 管理者専用・ロール限定ページ

呼び出し側のページ
  └── redirect / notFound の判断は呼び出し側に持たせない
      (requireSession / requireRole が引き受ける)

よくある誤用

getSession() に redirect を内包させる

// ❌ getSession() が redirect を持つと、未認証でも使えるページで呼べなくなる
export async function getSession() {
  const session = await fetchSession();
  if (!session) redirect("/login"); // 呼び出し元を強制する
  return session;
}

// ✅ redirect は requireSession() に分離する
export async function getSession(): Promise<Session | null> { ... }
export async function requireSession(): Promise<Session> {
  const s = await getSession();
  if (!s) redirect("/login");
  return s;
}

同一リクエストで getSession() を何度も呼ぶ

// ❌ レイアウト・ページ・コンポーネントでそれぞれ呼ぶと DB クエリが重複する
// Layout: await getSession()
// Page: await getSession()   ← 同じリクエスト内で再実行される

// ✅ React cache() でメモ化すると 1 リクエスト = 1 クエリになる
export const getSession = cache(async () => { ... });

未認証・権限不足での失敗分岐 → 認証・権限チェックの失敗分岐

認証判定をどこに置くか → Next.js 認証方式比較

関連ドキュメント

関連サンプル

同じテーマや技術スタックを使った実装例