🚀

Next.js 15 App Routerの実践的な使い方 - SaaS開発のパターンから学ぶ

に公開

はじめに

Next.js 15のApp Routerは、従来のPages Routerから大きく進化し、Server Components、Streaming、並列データフェッチなど、パフォーマンスとDXを向上させる機能が追加されました。

この記事では、配送ルート最適化SaaSの開発パターンを通じて、App Routerの実践的な使い方を解説します。実際のプロダクション環境での設計パターンを紹介しますが、セキュリティ上の理由から一部の実装は簡略化・抽象化しています。

この記事で学べること

  • ✅ App Routerのディレクトリ構成(Route Groups、Dynamic Routes)
  • ✅ Server ComponentsとClient Componentsの使い分け
  • ✅ Route Handlers(API Routes)の実装パターン
  • ✅ Metadata APIでのSEO最適化
  • ✅ Middlewareでの認証パターン
  • ✅ Nested Layoutsでの認証保護パターン

App Routerのディレクトリ構成

典型的なSaaSアプリケーションでは、以下のようなApp Routerのディレクトリ構成を採用できます。

app/
├── layout.tsx                 # ルートレイアウト(全ページ共通)
├── page.tsx                   # トップページ(ランディングページ)
├── middleware.ts              # ミドルウェア(認証等)
│
├── (auth)/                    # 認証ページグループ
│   ├── layout.tsx            # 認証ページ用レイアウト
│   ├── login/page.tsx        # ログインページ
│   ├── signup/page.tsx       # サインアップページ
│   └── reset-password/page.tsx # パスワードリセット
│
├── (dashboard)/              # ダッシュボードグループ(要認証)
│   ├── layout.tsx            # ダッシュボード用レイアウト(認証保護)
│   ├── dashboard/page.tsx    # ダッシュボード
│   ├── items/
│   │   ├── page.tsx          # アイテム一覧
│   │   ├── new/page.tsx      # アイテム作成
│   │   └── [id]/page.tsx     # アイテム詳細(動的ルート)
│   ├── subscription/page.tsx # サブスクリプション管理
│   └── profile/page.tsx      # プロフィール
│
├── (marketing)/              # マーケティングページグループ
│   ├── layout.tsx            # マーケティングページ用レイアウト
│   ├── pricing/page.tsx      # 料金プラン
│   ├── terms/page.tsx        # 利用規約
│   └── privacy/page.tsx      # プライバシーポリシー
│
└── api/                      # API Routes(Route Handlers)
    ├── items/
    │   ├── route.ts          # GET/POST /api/items
    │   └── [id]/route.ts     # GET/PUT/DELETE /api/items/:id
    ├── subscription/
    │   └── checkout/route.ts # POST /api/subscription/checkout
    └── webhooks/
        └── stripe/route.ts   # POST /api/webhooks/stripe

Route Groups(ルートグループ)の活用

(auth), (dashboard), (marketing) のような 括弧で囲まれたディレクトリRoute Groups と呼ばれ、URLパスには影響しないレイアウト分離の仕組みです。

なぜSaaSにRoute Groupsが向いているのか?

SaaSアプリケーションでは、ユーザー種別ごとに異なるUXを提供する必要があります:

  • 🔓 未ログインユーザー: マーケティングページ(料金プラン、LP等)→ シンプルなヘッダー・フッター
  • 🔐 ログイン済みユーザー: ダッシュボード → 機能豊富なナビゲーション、ユーザーメニュー
  • 📝 認証フロー: ログイン・サインアップ → 最小限のUI、フォーカス重視

Route Groupsを使用することで:

  1. 異なるレイアウトを適用: 認証ページ、ダッシュボード、マーケティングページで異なるヘッダー・フッターを表示
  2. 認証保護の範囲を明確化: (dashboard) グループ全体を認証保護
  3. URLをシンプルに保つ: /dashboard/items ではなく /items のままでOK
  4. チーム開発の効率化: ディレクトリ構造で責任範囲が明確(マーケティングチーム vs プロダクトチーム)

実装例:(dashboard) グループのレイアウト

app/(dashboard)/layout.tsx
import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
import { DashboardHeader, DashboardFooter } from '@/components/dashboard';

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <ProtectedRoute>
      <div className="flex min-h-screen flex-col bg-gray-50">
        <DashboardHeader />
        <main className="container mx-auto flex-1 px-4 py-8">
          {children}
        </main>
        <DashboardFooter />
      </div>
    </ProtectedRoute>
  );
}

ポイント:

  • <ProtectedRoute> で全ページを認証保護
  • (dashboard) グループ内の全ページが自動的にこのレイアウトを継承
  • ヘッダー・フッターを共有し、コードの重複を削減

実装例:(auth) グループのレイアウト

app/(auth)/layout.tsx
import Link from 'next/link';

export default function AuthLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex min-h-screen flex-col bg-gray-50">
      {/* Header */}
      <header className="border-b bg-white">
        <div className="container mx-auto px-4">
          <div className="flex h-16 items-center justify-between">
            <Link href="/" className="text-2xl font-bold text-blue-500">
              YourApp
            </Link>
            <Link href="/" className="text-sm text-gray-600 hover:underline">
              ← トップページに戻る
            </Link>
          </div>
        </div>
      </header>

      {/* Main Content */}
      <main className="flex-1">{children}</main>
    </div>
  );
}

ポイント:

  • 認証ページ専用のシンプルなヘッダー(ロゴ + トップページへのリンク)
  • ダッシュボードとは異なるレイアウト
  • 認証保護は不要(未ログインユーザーがアクセス)

Server ComponentsとClient Componentsの使い分け

App Routerでは、デフォルトでServer Componentsが使用されます。インタラクティブな機能が必要な場合のみ 'use client' ディレクティブを追加します。

なぜSaaSにServer Componentsが重要か

SaaSアプリケーションでは、初期表示速度SEOがビジネスの成否を分けます。Server Componentsを適切に使用することで:

  • バンドルサイズの削減: クライアントに送信するJavaScriptが大幅に減少(50-70%削減も可能)
  • 初期ロードの高速化: サーバーでHTMLを生成するため、Time to First Byte(TTFB)が向上
  • SEO最適化: 検索エンジンが完全なHTMLをクロール可能
  • サーバーコストの最適化: サーバーサイドで処理を完結させ、クライアント側の負荷を軽減

特に、ランディングページダッシュボードの初期表示は、Server Componentsで実装することで、ユーザー体験が大幅に向上します。

Server Componentsの例:トップページ

app/page.tsx
import { Hero } from '@/components/marketing/Hero';
import { Features } from '@/components/marketing/Features';
import { Pricing } from '@/components/marketing/Pricing';

export default function Home() {
  // 構造化データ(JSON-LD)
  const webAppSchema = {
    '@context': 'https://schema.org',
    '@type': 'WebApplication',
    name: 'YourApp',
    description: 'SaaSアプリケーションの説明',
    applicationCategory: 'BusinessApplication',
    offers: {
      '@type': 'Offer',
      price: '1980',
      priceCurrency: 'JPY',
    },
  };

  return (
    <div className="flex min-h-screen flex-col">
      {/* JSON-LD for SEO */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify(webAppSchema),
        }}
      />

      <main>
        <Hero />
        <Features />
        <Pricing />
      </main>
    </div>
  );
}

Server Componentsのメリット:

  • ✅ JavaScriptバンドルサイズが小さい(クライアントに送信されるJSが減る)
  • ✅ SEOに強い(サーバー側でHTMLが生成される)
  • ✅ 初期ロードが速い

Client Componentsの例:アイテム一覧ページ

app/(dashboard)/items/page.tsx
'use client'; // ← Client Componentディレクティブ

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
import { Button } from '@/components/ui/button';

export default function ItemsPage() {
  const { user } = useAuth();
  const router = useRouter();

  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [searchQuery, setSearchQuery] = useState('');

  useEffect(() => {
    fetchItems();
  }, []);

  const fetchItems = async () => {
    setIsLoading(true);
    try {
      const response = await fetch('/api/items', {
        headers: {
          'Authorization': `Bearer ${await user.getIdToken()}`,
        },
      });
      const data = await response.json();
      setItems(data.items);
    } finally {
      setIsLoading(false);
    }
  };

  // フィルター処理
  const filteredItems = items.filter(item =>
    item.name.toLowerCase().includes(searchQuery.toLowerCase())
  );

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-3xl font-bold">アイテム一覧</h1>
        <Button onClick={() => router.push('/items/new')}>
          新規作成
        </Button>
      </div>

      {/* 検索UI */}
      <input
        type="text"
        placeholder="検索..."
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        className="w-full px-4 py-2 border rounded"
      />

      {/* アイテム一覧 */}
      {isLoading ? (
        <div>読み込み中...</div>
      ) : (
        <div className="grid gap-4">
          {filteredItems.map((item) => (
            <div key={item.id} className="p-4 border rounded">
              <h3>{item.name}</h3>
              <p>{item.description}</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Client Componentsが必要な理由:

  • useState, useEffect, useRouter などのReact Hooksを使用
  • ❗ ユーザーのインタラクション(検索、フィルター)を処理
  • ❗ 動的な状態管理が必要

Route Handlers(API Routes)の実装

App Routerでは、route.ts ファイルで GET, POST, PUT, DELETE などのHTTPメソッドをエクスポートすることで、API Routesを実装します。

なぜSaaSにRoute Handlersが適しているか

SaaSアプリケーションでは、フロントエンドとバックエンドを1つのリポジトリで管理することが開発効率とメンテナンス性を大幅に向上させます。Route Handlersを使用することで:

  • モノリポ不要: Next.jsのみでフルスタック開発が可能
  • 型安全性: フロントエンドとバックエンドで同じTypeScript型を共有
  • 認証の一元管理: Firebaseトークン検証、JWT検証等をAPI層で実装
  • バリデーションの統一: Zodなどのスキーマライブラリで入力検証を一元管理
  • デプロイの簡素化: Vercelにデプロイするだけでフロントエンド+APIが稼働

特に、マルチテナント型SaaSでは、ユーザー認証→データフィルタリングのフローをRoute Handlersで統一的に実装できます。

基本的な実装パターン:一覧取得・作成API

app/api/items/route.ts
import { NextRequest, NextResponse } from 'next/server';

/**
 * GET /api/items
 * アイテム一覧を取得
 */
export async function GET(request: NextRequest) {
  try {
    // 認証確認
    const authHeader = request.headers.get('authorization');
    if (!authHeader?.startsWith('Bearer ')) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }

    const token = authHeader.split('Bearer ')[1];

    // トークン検証(実装は省略)
    const userId = await verifyToken(token);

    // クエリパラメータ取得
    const { searchParams } = new URL(request.url);
    const page = parseInt(searchParams.get('page') || '1', 10);
    const limit = Math.min(parseInt(searchParams.get('limit') || '20', 10), 100);

    // データベースから取得(実装は省略)
    const items = await fetchItemsFromDB(userId, { page, limit });
    const total = await countItems(userId);

    return NextResponse.json({
      items,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    });
  } catch (error) {
    console.error('Get items error:', error);
    return NextResponse.json(
      { error: 'Internal Server Error' },
      { status: 500 }
    );
  }
}

/**
 * POST /api/items
 * アイテムを作成
 */
export async function POST(request: NextRequest) {
  try {
    // 認証確認
    const authHeader = request.headers.get('authorization');
    if (!authHeader?.startsWith('Bearer ')) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }

    const token = authHeader.split('Bearer ')[1];
    const userId = await verifyToken(token);

    // リクエストボディ取得
    const body = await request.json();
    const { name, description } = body;

    // バリデーション
    if (!name || name.length < 2 || name.length > 100) {
      return NextResponse.json(
        { error: 'Name must be between 2 and 100 characters' },
        { status: 400 }
      );
    }

    // データベースに保存(実装は省略)
    const item = await createItem(userId, { name, description });

    return NextResponse.json({ item }, { status: 201 });
  } catch (error) {
    console.error('Create item error:', error);
    return NextResponse.json(
      { error: 'Internal Server Error' },
      { status: 500 }
    );
  }
}

// ヘルパー関数(実装は省略)
async function verifyToken(token: string): Promise<string> {
  // Firebase Admin SDK等でトークンを検証
  // 実装は環境により異なる
  throw new Error('Not implemented');
}

async function fetchItemsFromDB(userId: string, options: any) {
  // データベースからアイテムを取得
  throw new Error('Not implemented');
}

async function countItems(userId: string): Promise<number> {
  // アイテム数をカウント
  throw new Error('Not implemented');
}

async function createItem(userId: string, data: any) {
  // アイテムを作成
  throw new Error('Not implemented');
}

ポイント:

  • GETPOST を同じファイルでエクスポート
  • ✅ 認証トークンを検証(Bearer トークン)
  • ✅ クエリパラメータ(page, limit)でページネーション対応
  • ✅ バリデーション、エラーハンドリングを実施
  • ✅ エラーレスポンスを統一

Metadata APIでのSEO最適化

App Routerでは、Metadata API を使用してSEOを最適化できます。

なぜSaaSにMetadata APIが重要か

SaaSアプリケーションでは、オーガニック検索からの流入がビジネス成長の鍵となります。Metadata APIを適切に設定することで:

  • 検索エンジン最適化: Google、Bing等の検索結果で上位表示
  • ソーシャルシェア: Twitter、Facebook等でのシェア時に適切なプレビューを表示
  • ブランディング: 一貫したタイトル・説明文でブランド認知を向上

特に、title.template を使用することで、全ページのタイトルを統一フォーマットで管理でき、ブランド一貫性を保てます。

ルートレイアウトでのメタデータ設定

app/layout.tsx
import type { Metadata } from "next";
import { AuthProvider } from "@/contexts/AuthContext";
import "./globals.css";

export const metadata: Metadata = {
  title: {
    default: "YourApp - SaaSアプリケーション",
    template: "%s | YourApp",
  },
  description: "SaaSアプリケーションの説明文",
  keywords: ["SaaS", "業務効率化", "クラウド"],
  metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL || "https://yourapp.com"),
  openGraph: {
    type: "website",
    locale: "ja_JP",
    url: "/",
    title: "YourApp - SaaSアプリケーション",
    description: "SaaSアプリケーションの説明文",
    siteName: "YourApp",
  },
  twitter: {
    card: "summary_large_image",
    title: "YourApp - SaaSアプリケーション",
    description: "SaaSアプリケーションの説明文",
  },
  robots: {
    index: true,
    follow: true,
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  );
}

ポイント:

  • title.template で子ページのタイトルを動的に生成(例: "ダッシュボード | YourApp")
  • ✅ Open Graph、Twitterカード対応
  • ✅ robots メタタグで検索エンジンのクローリング制御
  • metadataBase で相対URLを絶対URLに変換

よくある落とし穴とパフォーマンス上の注意点

パフォーマンス最適化のヒント

1. 画像サイズの最適化:

  • Open Graph画像は 1200x630px を推奨
  • ファイルサイズは 300KB以下 に抑える
  • WebP形式を使用してさらに軽量化

2. メタデータのキャッシュ:

  • 動的メタデータは revalidate を使ってキャッシュ
export const revalidate = 3600; // 1時間キャッシュ

export async function generateMetadata({ params }) {
  const data = await fetchData(params.id);
  return { title: data.title };
}

3. 不要なメタデータは削除:

  • 使用しないSNS用のメタデータは削除してバンドルサイズを削減

Middlewareでの認証パターン

Next.js 15では、Middleware を使用してリクエスト単位で処理を実行できます。

なぜSaaSにMiddlewareが有効か

Middlewareは、全リクエストの前処理を一元管理できるため、SaaSアプリケーションで有効です:

  • 認証の一元管理: すべてのページで認証チェックを実行(/api/* は除外可能)
  • A/Bテスト: リクエストヘッダーやCookieに基づいて動的にページを出し分け
  • 地域制限: IPアドレスや地域情報に基づいてアクセス制御
  • 開発環境の保護: Basic認証で開発環境を保護

ただし、パフォーマンス影響があるため、適切に使用することが重要です。

開発環境でのBasic認証

middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  // APIエンドポイントはスキップ(各APIが独自の認証を持つため)
  if (request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }

  // Basic認証を適用(開発環境のみ)
  if (process.env.ENABLE_BASIC_AUTH === 'true') {
    const basicAuth = request.headers.get('authorization');

    if (basicAuth) {
      const authValue = basicAuth.split(' ')[1];
      const [user, pwd] = atob(authValue).split(':');

      const validUser = process.env.BASIC_AUTH_USER || 'admin';
      const validPassword = process.env.BASIC_AUTH_PASSWORD || 'password';

      if (user === validUser && pwd === validPassword) {
        return NextResponse.next();
      }
    }

    // 認証失敗時は401を返す
    return new NextResponse('Authentication required', {
      status: 401,
      headers: {
        'WWW-Authenticate': 'Basic realm="Secure Area"',
      },
    });
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    // 静的ファイルを除外
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

ポイント:

  • ✅ 開発環境でのBasic認証(ENABLE_BASIC_AUTH=true 時のみ)
  • /api/* はスキップ(各APIが独自の認証を持つため)
  • matcher で静的ファイルを除外

よくある落とし穴とパフォーマンス上の注意点

パフォーマンス最適化のヒント

1. 処理は最小限に:

  • Middlewareは全リクエストで実行されるため、10ms以上かかる処理は避ける
  • データベースクエリ、外部API呼び出しは 絶対に避ける
  • 認証トークンの検証程度に留める

2. 早期リターン:

  • 静的ファイル、API Routes等は早期にスキップ
export function middleware(request: NextRequest) {
  // 早期リターン
  if (request.nextUrl.pathname.startsWith('/api/')) {
    return NextResponse.next();
  }
  if (request.nextUrl.pathname.startsWith('/_next/')) {
    return NextResponse.next();
  }

  // 認証処理...
}

3. Cookieの使用:

  • 認証状態はCookieに保存し、Middlewareで高速に検証
  • セッションストアへのアクセスは避ける

4. Vercel Edge Networkの活用:

  • Middlewareは世界中のEdge Networkで実行される
  • ユーザーに最も近いリージョンで処理されるため、レイテンシが低い

Nested Layoutsでの認証保護パターン

App Routerでは、Nested Layouts(ネストされたレイアウト) を使用して、特定のページグループに認証保護を適用できます。

なぜSaaSにNested Layoutsが有効か

SaaSアプリケーションでは、認証済みユーザー専用エリア公開エリアを明確に分ける必要があります。Nested Layoutsを使用することで:

  • 認証ロジックの集約: (dashboard) Route Group内の全ページに自動的に認証保護を適用
  • コードの重複削減: 各ページで認証チェックを書く必要がない
  • UI/UXの一貫性: ダッシュボード全体で統一されたヘッダー・ナビゲーション
  • 保守性の向上: 認証ロジックの変更が1箇所で完結

特に、フリーミアムモデルのSaaSでは、未認証ユーザーにはLP、認証済みユーザーにはダッシュボードを表示する、という切り分けがNested Layoutsで簡単に実装できます。

実装例:ProtectedRoute

components/auth/ProtectedRoute.tsx
'use client';

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';

interface ProtectedRouteProps {
  children: React.ReactNode;
}

export function ProtectedRoute({ children }: ProtectedRouteProps) {
  const { user, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !user) {
      // 未認証の場合はログインページにリダイレクト
      router.push('/login');
    }
  }, [user, loading, router]);

  // ロード中は何も表示しない
  if (loading) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-gray-500">読み込み中...</div>
      </div>
    );
  }

  // 認証済みの場合のみ子要素を表示
  if (!user) {
    return null;
  }

  return <>{children}</>;
}

AuthProvider(認証状態管理)

contexts/AuthContext.tsx (抜粋)
'use client';

import { createContext, useContext, useState, useEffect } from 'react';

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 認証状態を監視(Firebase等)
    const unsubscribe = onAuthStateChanged(async (authUser) => {
      if (authUser) {
        // ログイン済み
        setUser(authUser);
      } else {
        // 未ログイン
        setUser(null);
      }
      setLoading(false);
    });

    return () => unsubscribe();
  }, []);

  const value = {
    user,
    loading,
    // ... その他の認証関数
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// onAuthStateChanged の実装は認証プロバイダー(Firebase等)に依存
function onAuthStateChanged(callback: (user: any) => void) {
  // 実装は省略
  return () => {};
}

ポイント:

  • ✅ 認証状態を監視(Firebase Authentication等)
  • ✅ ログイン中は自動的にユーザー情報を取得
  • loading 状態でローディング表示を制御
  • ✅ 未認証の場合は /login にリダイレクト

Dynamic Routes(動的ルート)

App Routerでは、[param] という命名規則でDynamic Routesを作成できます。

なぜSaaSにDynamic Routesが必須か

SaaSアプリケーションでは、ユーザーごとに異なるデータを扱うため、Dynamic Routesが必須です:

  • リソースベースのURL: /items/123, /projects/abc のように、リソースIDをURLに含められる
  • SEO対策: 静的ルートと同様に検索エンジンがクロール可能
  • 共有・ブックマーク: ユーザーが特定のリソースURLを共有・ブックマーク可能
  • RESTful設計: /api/items/[id] のようなRESTful APIとフロントエンドのルートを統一

特に、B2Bサブスクリプション型SaaSでは、プロジェクト、タスク、レポートなど、多数のリソース詳細ページが必要になります。Dynamic Routesを使用することで、スケーラブルなURL設計が可能です。

実装例:アイテム詳細ページ

app/(dashboard)/items/[id]/page.tsx
'use client';

import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';

export default function ItemDetailPage() {
  const params = useParams();
  const itemId = params.id as string; // ← URLパラメータから itemId を取得

  const [item, setItem] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetchItem();
  }, [itemId]);

  const fetchItem = async () => {
    setIsLoading(true);
    try {
      const response = await fetch(`/api/items/${itemId}`);
      const data = await response.json();
      setItem(data.item);
    } finally {
      setIsLoading(false);
    }
  };

  if (isLoading) {
    return <div>読み込み中...</div>;
  }

  if (!item) {
    return <div>アイテムが見つかりません</div>;
  }

  return (
    <div className="space-y-6">
      <h1 className="text-3xl font-bold">{item.name}</h1>
      <p>{item.description}</p>
      {/* ... */}
    </div>
  );
}

ポイント:

  • useParams() でURLパラメータ(id)を取得
  • /items/abc123params.id"abc123" になる
  • ✅ 動的にAPIからデータを取得して表示

画面遷移図(マーメイド)

典型的なSaaSアプリケーションの画面遷移をマーメイド図で可視化します。

画面遷移のポイント:

  1. トップページ(/:

    • ログイン/サインアップへの導線
    • 料金プラン、利用規約等へのリンク
  2. 認証後(/dashboard:

    • (dashboard) Route Group内の全ページにアクセス可能
    • 未認証の場合は自動的に /login にリダイレクト
  3. アイテム管理フロー:

    • アイテム一覧 → アイテム詳細
    • アイテム作成 → アイテム詳細
  4. 決済フロー:

    • サブスクリプション管理 → Stripe Checkout → サブスクリプション管理

まとめ

この記事では、SaaS開発のパターンを通じて、Next.js 15 App Routerの実践的な使い方を解説しました。

学んだこと

  • Route Groups で異なるレイアウトを適用(認証ページ、ダッシュボード、マーケティングページ)
  • Server Components をデフォルトで使用し、必要な場合のみ Client Components を使用
  • Route Handlers で GET/POST/PUT/DELETE エンドポイントを実装
  • Metadata API で SEO 最適化(Open Graph、Twitter カード)
  • Middleware で Basic 認証を実装
  • Nested Layouts で認証保護パターンを実装
  • Dynamic Routes で動的ルート(/items/[id])を実装

さらに学びたい方へ


Delivroute について

この記事で紹介したパターンは、Delivrouteというサービスの開発で実際に実践されています。

Delivrouteは、配送ルート最適化を簡単に行えるWebアプリケーションです。

  • 🗺️ 最大10地点のルート最適化: Google Maps Routes APIで最適な配送順序を自動計算
  • 簡単CSVインポート: CSVファイルから一括で配送先を登録
  • 💰 クレジット制: 無料プラン(月3クレジット)、Proプラン(月60クレジット)で気軽に利用可能
  • 🔥 Next.js 15 + Firebase: この記事で紹介した技術スタックで構築

👉 サービスはこちら: https://delivroute.com


この記事が役に立ったら、ぜひいいね👍やコメント💬をお願いします!

Discussion