メインコンテンツへスキップ
#Web制作 約7分で読めます

Next.js 15 Dynamic API Routes で複雑なバックエンド処理を効率化する実装ガイド

Next.js 15のDynamic API Routesを活用した柔軟なバックエンドアーキテクチャの構築方法を解説。Catch-all Segments、Optional Segments、並列処理の実装パターンを具体的なコード例で紹介します。

Next.js 15 Dynamic API Routesとは

Next.js 15のApp RouterにおけるDynamic API Routesは、URLパスの一部を動的なパラメータとして扱うことで、柔軟なAPIエンドポイントを構築できる機能です。従来のPages Routerではapi/users/[id].tsのような形式でしたが、App Routerではapp/api/users/[id]/route.tsとして実装します。

Next.js 15(2024年10月リリース)では、App Routerが安定版となり、Dynamic SegmentsとAPI Routesの組み合わせがより堅牢になりました。特に、Catch-all SegmentsやOptional Catch-all Segmentsを使うことで、複雑なバックエンドロジックを単一のエンドポイントで処理できるようになっています。

この記事では、Next.js 15のDynamic API Routesを活用した実践的なバックエンド実装パターンを、認証、エラーハンドリング、並列処理まで含めて解説します。

Dynamic Segmentsの3つの種類と使い分け

Next.js 15のDynamic API Routesでは、以下の3種類のSegment形式を使い分けることで、多様なルーティングパターンを実現できます。

1. 通常のDynamic Segment: [id]

最もシンプルな形式で、単一のパラメータを受け取ります。

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const userId = params.id;
  
  // データベースからユーザー情報を取得
  const user = await db.user.findUnique({
    where: { id: userId },
  });

  if (!user) {
    return NextResponse.json(
      { error: 'User not found' },
      { status: 404 }
    );
  }

  return NextResponse.json(user);
}

使用例: /api/users/123params.id = "123"

2. Catch-all Segments: [...slug]

複数のパスセグメントを配列として受け取ります。階層的なリソースやネストしたエンドポイントに最適です。

// app/api/products/[...slug]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: { slug: string[] } }
) {
  const [category, subcategory, productId] = params.slug;

  // カテゴリのみの場合: /api/products/electronics
  if (params.slug.length === 1) {
    const products = await db.product.findMany({
      where: { category },
    });
    return NextResponse.json(products);
  }

  // サブカテゴリまで指定: /api/products/electronics/laptops
  if (params.slug.length === 2) {
    const products = await db.product.findMany({
      where: { category, subcategory },
    });
    return NextResponse.json(products);
  }

  // 商品IDまで指定: /api/products/electronics/laptops/abc123
  if (params.slug.length === 3) {
    const product = await db.product.findUnique({
      where: { id: productId },
    });
    return NextResponse.json(product);
  }

  return NextResponse.json(
    { error: 'Invalid path' },
    { status: 400 }
  );
}

使用例:

  • /api/products/electronicsslug = ["electronics"]
  • /api/products/electronics/laptops/abc123slug = ["electronics", "laptops", "abc123"]

3. Optional Catch-all Segments: [[...slug]]

Catch-allの拡張版で、パラメータがない場合も同じエンドポイントで処理できます。

// app/api/search/[[...params]]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: { params?: string[] } }
) {
  const searchParams = params.params || [];

  // パラメータなし: /api/search → 全件検索
  if (searchParams.length === 0) {
    const results = await db.item.findMany({
      take: 100,
    });
    return NextResponse.json(results);
  }

  // /api/search/keyword/laptop
  const [type, value] = searchParams;
  
  const results = await db.item.findMany({
    where: {
      [type]: {
        contains: value,
        mode: 'insensitive',
      },
    },
  });

  return NextResponse.json(results);
}

使用例:

  • /api/searchparams = undefined
  • /api/search/keyword/laptopparams = ["keyword", "laptop"]
flowchart TD
    A["リクエスト受信"] --> B{"パス形式判定"}
    B -->|"単一パラメータ"| C["[id]<br/>通常のDynamic Segment"]
    B -->|"階層構造"| D["[...slug]<br/>Catch-all Segments"]
    B -->|"階層構造<br/>(オプション)"| E["[[...slug]]<br/>Optional Catch-all"]
    
    C --> F["params.id: string"]
    D --> G["params.slug: string[]"]
    E --> H["params.slug?: string[]"]
    
    F --> I["ビジネスロジック実行"]
    G --> I
    H --> I
    
    I --> J["レスポンス返却"]

上図は、Dynamic Segmentsの3種類がどのようにパラメータを処理するかを示しています。

認証・認可を組み込んだAPI Route実装

実際のプロダクションでは、APIエンドポイントに認証・認可機能を組み込む必要があります。Next.js 15では、Middlewareと組み合わせることで効率的な実装が可能です。

JWTトークン検証の実装

// app/api/admin/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verify } from 'jsonwebtoken';

interface JWTPayload {
  userId: string;
  role: string;
}

async function verifyAuth(request: NextRequest): Promise<JWTPayload | null> {
  const authHeader = request.headers.get('authorization');
  
  if (!authHeader?.startsWith('Bearer ')) {
    return null;
  }

  const token = authHeader.substring(7);
  
  try {
    const payload = verify(token, process.env.JWT_SECRET!) as JWTPayload;
    return payload;
  } catch (error) {
    return null;
  }
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  // 認証チェック
  const auth = await verifyAuth(request);
  
  if (!auth) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  // 権限チェック(管理者のみ削除可能)
  if (auth.role !== 'admin') {
    return NextResponse.json(
      { error: 'Forbidden' },
      { status: 403 }
    );
  }

  // ユーザー削除処理
  await db.user.delete({
    where: { id: params.id },
  });

  return NextResponse.json(
    { message: 'User deleted successfully' },
    { status: 200 }
  );
}

Middlewareでの一括認証処理

複数のAPIエンドポイントで共通の認証処理が必要な場合、Middlewareを使うことでコードの重複を防げます。

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

export function middleware(request: NextRequest) {
  // 認証が必要なパスのパターン
  if (request.nextUrl.pathname.startsWith('/api/admin')) {
    const authHeader = request.headers.get('authorization');
    
    if (!authHeader?.startsWith('Bearer ')) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }

    const token = authHeader.substring(7);
    
    try {
      const payload = verify(token, process.env.JWT_SECRET!);
      
      // ペイロードをヘッダーに追加して後続処理で利用可能にする
      const requestHeaders = new Headers(request.headers);
      requestHeaders.set('x-user-id', (payload as any).userId);
      requestHeaders.set('x-user-role', (payload as any).role);
      
      return NextResponse.next({
        request: {
          headers: requestHeaders,
        },
      });
    } catch (error) {
      return NextResponse.json(
        { error: 'Invalid token' },
        { status: 401 }
      );
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/api/admin/:path*',
};
sequenceDiagram
    participant Client as クライアント
    participant MW as Middleware
    participant API as API Route
    participant DB as データベース

    Client->>MW: リクエスト + JWT
    MW->>MW: トークン検証
    
    alt トークン無効
        MW->>Client: 401 Unauthorized
    else トークン有効
        MW->>MW: ペイロード抽出
        MW->>API: リクエスト転送<br/>(ヘッダーに userId, role 付与)
        API->>API: 権限チェック
        
        alt 権限不足
            API->>Client: 403 Forbidden
        else 権限OK
            API->>DB: データ操作
            DB->>API: 結果返却
            API->>Client: 200 Success
        end
    end

上図は、MiddlewareとAPI Routeの連携による認証フローを示しています。

複雑なバックエンドロジックの実装パターン

Dynamic API Routesを使った高度な実装パターンを紹介します。

並列処理による高速化

複数のデータソースから情報を取得する場合、Promise.allを使った並列処理で大幅に高速化できます。

// app/api/dashboard/[userId]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: { userId: string } }
) {
  const userId = params.userId;

  try {
    // 複数のデータを並列取得
    const [user, orders, notifications, analytics] = await Promise.all([
      db.user.findUnique({ where: { id: userId } }),
      db.order.findMany({
        where: { userId },
        take: 10,
        orderBy: { createdAt: 'desc' },
      }),
      db.notification.findMany({
        where: { userId, read: false },
        take: 5,
      }),
      db.analytics.aggregate({
        where: { userId },
        _sum: { totalSpent: true },
        _count: { orderId: true },
      }),
    ]);

    return NextResponse.json({
      user,
      recentOrders: orders,
      unreadNotifications: notifications,
      stats: {
        totalOrders: analytics._count.orderId,
        totalSpent: analytics._sum.totalSpent,
      },
    });
  } catch (error) {
    console.error('Dashboard data fetch error:', error);
    return NextResponse.json(
      { error: 'Failed to fetch dashboard data' },
      { status: 500 }
    );
  }
}

バッチ処理APIの実装

複数のリソースを一度に操作するバッチAPIの実装例です。

// app/api/batch/[...operation]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

const batchSchema = z.object({
  items: z.array(z.object({
    id: z.string(),
    action: z.enum(['update', 'delete']),
    data: z.record(z.any()).optional(),
  })),
});

export async function POST(
  request: NextRequest,
  { params }: { params: { operation: string[] } }
) {
  const [resource] = params.operation;

  // リソースタイプの検証
  const validResources = ['users', 'products', 'orders'];
  if (!validResources.includes(resource)) {
    return NextResponse.json(
      { error: 'Invalid resource type' },
      { status: 400 }
    );
  }

  try {
    const body = await request.json();
    const { items } = batchSchema.parse(body);

    // バッチ処理をトランザクション内で実行
    const results = await db.$transaction(
      items.map(item => {
        if (item.action === 'update') {
          return db[resource].update({
            where: { id: item.id },
            data: item.data,
          });
        } else if (item.action === 'delete') {
          return db[resource].delete({
            where: { id: item.id },
          });
        }
      })
    );

    return NextResponse.json({
      success: true,
      processed: results.length,
      results,
    });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Invalid request body', details: error.errors },
        { status: 400 }
      );
    }

    console.error('Batch operation error:', error);
    return NextResponse.json(
      { error: 'Batch operation failed' },
      { status: 500 }
    );
  }
}

レート制限の実装

APIの過剰利用を防ぐためのレート制限を実装します。

// lib/rate-limit.ts
import { LRUCache } from 'lru-cache';

interface RateLimitOptions {
  interval: number; // ミリ秒
  uniqueTokenPerInterval: number;
}

export function rateLimit(options: RateLimitOptions) {
  const tokenCache = new LRUCache({
    max: options.uniqueTokenPerInterval,
    ttl: options.interval,
  });

  return {
    check: (token: string, limit: number): boolean => {
      const tokenCount = (tokenCache.get(token) as number[]) || [0];
      if (tokenCount[0] === 0) {
        tokenCache.set(token, [1]);
        return true;
      }

      tokenCount[0] += 1;

      const currentUsage = tokenCount[0];
      const isRateLimited = currentUsage >= limit;

      tokenCache.set(token, tokenCount);

      return !isRateLimited;
    },
  };
}

// app/api/search/[...params]/route.ts
import { rateLimit } from '@/lib/rate-limit';

const limiter = rateLimit({
  interval: 60 * 1000, // 60秒
  uniqueTokenPerInterval: 500,
});

export async function GET(request: NextRequest) {
  const ip = request.ip ?? 'anonymous';
  
  const isAllowed = limiter.check(ip, 10); // 1分間に10リクエストまで
  
  if (!isAllowed) {
    return NextResponse.json(
      { error: 'Rate limit exceeded' },
      { status: 429 }
    );
  }

  // 通常の処理
  // ...
}

エラーハンドリングとログ管理

本番環境で運用するAPIには、適切なエラーハンドリングとログ管理が不可欠です。

型安全なエラーレスポンス

// lib/api-error.ts
export class ApiError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public code?: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

export function handleApiError(error: unknown): NextResponse {
  if (error instanceof ApiError) {
    return NextResponse.json(
      {
        error: error.message,
        code: error.code,
      },
      { status: error.statusCode }
    );
  }

  if (error instanceof z.ZodError) {
    return NextResponse.json(
      {
        error: 'Validation failed',
        details: error.errors,
      },
      { status: 400 }
    );
  }

  // 予期しないエラー
  console.error('Unexpected error:', error);
  return NextResponse.json(
    { error: 'Internal server error' },
    { status: 500 }
  );
}

// app/api/users/[id]/route.ts
import { ApiError, handleApiError } from '@/lib/api-error';

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const user = await db.user.findUnique({
      where: { id: params.id },
    });

    if (!user) {
      throw new ApiError(404, 'User not found', 'USER_NOT_FOUND');
    }

    return NextResponse.json(user);
  } catch (error) {
    return handleApiError(error);
  }
}

構造化ログの実装

// lib/logger.ts
import pino from 'pino';

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => {
      return { level: label };
    },
  },
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
    },
  },
});

// app/api/orders/[orderId]/route.ts
import { logger } from '@/lib/logger';

export async function POST(
  request: NextRequest,
  { params }: { params: { orderId: string } }
) {
  const startTime = Date.now();

  try {
    const body = await request.json();

    logger.info({
      msg: 'Order processing started',
      orderId: params.orderId,
      userId: body.userId,
    });

    const order = await processOrder(params.orderId, body);

    const duration = Date.now() - startTime;

    logger.info({
      msg: 'Order processed successfully',
      orderId: params.orderId,
      duration,
    });

    return NextResponse.json(order);
  } catch (error) {
    logger.error({
      msg: 'Order processing failed',
      orderId: params.orderId,
      error: error instanceof Error ? error.message : 'Unknown error',
      duration: Date.now() - startTime,
    });

    return handleApiError(error);
  }
}

まとめ

Next.js 15のDynamic API Routesを活用することで、以下のような柔軟なバックエンド処理が実現できます。

  • 3種類のDynamic Segmentsを使い分けて多様なルーティングパターンに対応
  • Middlewareと組み合わせた認証・認可で安全なAPIを構築
  • 並列処理とバッチ処理でパフォーマンスを最適化
  • レート制限でAPIの過剰利用を防止
  • 型安全なエラーハンドリング構造化ログで運用品質を向上

Next.js 15のApp Routerは安定版となり、本番環境での採用事例も増えています。Dynamic API Routesを正しく理解し、認証、エラーハンドリング、ログ管理まで含めた実装パターンを身につけることで、スケーラブルで保守性の高いバックエンドシステムを構築できます。

参考リンク

#Next.js #API Routes #バックエンド #TypeScript #App Router
シェア