Next.jsのフレームワーク構造を整理した話

Next.jsでアプリを開発していると、「このコードはどこに書くべきか」で迷うことがあります。 この記事では、自分なりに整理した層構造とそれぞれの役割をまとめます。

全体像

処理の流れは以下のとおりです。

Component(UI)
   ↓ Hookを呼ぶ
Hook(状態管理・ロジック)
   ↓ /api/... にfetch
API Route Handler(サーバの入口)
   ↓ UseCaseを呼ぶ(省略可)
UseCase(複雑な処理フローの場合のみ)
   ↓ Serviceを呼ぶ
Service(外部API通信)
   ↓
外部API

UseCaseはAPIからServiceを呼び出すだけであれば省略できます。複数のServiceを組み合わせるなど、処理の流れが複雑になる場合に追加します。

ディレクトリ構成

タスク管理アプリを例にした構成です。

src/
  app/
    api/
      tasks/
        route.ts          # GET(一覧取得)・POST(新規作成)
        [id]/
          route.ts        # PATCH(更新)・DELETE(削除)
    tasks/
      page.tsx            # タスク一覧ページ
      [id]/
        page.tsx          # タスク詳細ページ
    layout.tsx
    page.tsx              # トップページ

  components/
    tasks/
      TaskList.tsx        # タスク一覧の表示
      TaskCard.tsx        # タスク1件の表示
      TaskForm.tsx        # タスク作成・編集フォーム

  hooks/
    useTasks.ts           # タスク一覧の取得・状態管理
    useTaskForm.ts        # フォームの入力状態管理

  usecases/
    createTask.ts         # タスク作成(通知送信など複数処理を伴う場合)

  services/
    taskService.ts        # タスクのCRUD(DBや外部APIと通信)
    notificationService.ts # 通知送信(メールAPIなど)

  lib/
    db.ts                 # DB接続
    utils/
      date.ts             # 日付フォーマットなど汎用関数

  types/
    task.ts               # Task型の定義

app/ はNext.jsのルーティング専用で、app/ 以外がアプリケーションコードです。

Component(UI層)― 表示専門

配置: src/components/

画面の表示とユーザー操作の受け付けを担当します。データの取得や加工はHookに任せ、受け取った値を表示することに専念します。

やらないこと

  • fetch を直接書かない
  • ビジネスロジックを書かない
// src/components/tasks/TaskList.tsx
"use client";
import { useTasks } from "@/hooks/useTasks";
import { TaskCard } from "./TaskCard";

export const TaskList = () => {
  const { tasks, loading, error } = useTasks();

  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラーが発生しました</p>;

  return (
    <ul>
      {tasks.map(task => (
        <TaskCard key={task.id} task={task} />
      ))}
    </ul>
  );
};

Hook(状態管理・ロジック層)― 画面ロジック管理者

配置: src/hooks/

useState / useEffect の管理、/api/... へのfetch、データ整形、ローディング・エラー状態の管理を担当します。ComponentとAPI Route Handlerの橋渡し役です。

やらないこと

  • 外部APIのURLを直書きしない(/api/... は書いてよい)
  • 秘密情報(APIキーなど)を扱わない
// src/hooks/useTasks.ts
import { useState, useEffect } from "react";
import { Task } from "@/types/task";

export const useTasks = () => {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch("/api/tasks")
      .then(res => {
        if (!res.ok) throw new Error("取得失敗");
        return res.json();
      })
      .then((data: Task[]) => setTasks(data))
      .catch(e => setError(e.message))
      .finally(() => setLoading(false));
  }, []);

  return { tasks, loading, error };
};

API Route Handler(サーバの入口)― クライアントとサーバの境界

配置: src/app/api/

クライアントからのリクエストを受け取り、ServiceまたはUseCaseを呼び出して結果を返します。APIキーなどの秘密情報はここより内側(サーバ側)でのみ扱います。

やらないこと

  • UIロジックを書かない
  • useState を使わない
  • ビジネスロジックを直接書かない(Serviceに委譲する)
// src/app/api/tasks/route.ts
import { getAllTasks, createTask } from "@/services/taskService";
import { NextRequest } from "next/server";

export async function GET() {
  const tasks = await getAllTasks();
  return Response.json(tasks);
}

export async function POST(req: NextRequest) {
  const body = await req.json();
  const task = await createTask(body);
  return Response.json(task, { status: 201 });
}

UseCase(ビジネスロジック層)― 複数処理の調整役

配置: src/usecases/

複数のServiceを組み合わせて一連の処理を実現します。「タスクを作成して、完了したら通知を送る」のように、単一のServiceでは表現できない処理フローをまとめる層です。シンプルな処理であれば省略してAPIから直接Serviceを呼んでかまいません。

やらないこと

  • UIロジックを書かない
  • useState を使わない
  • 外部APIに直接アクセスしない(Serviceに委譲する)
// src/usecases/createTask.ts
import { createTask } from "@/services/taskService";
import { sendNotification } from "@/services/notificationService";
import { formatDate } from "@/lib/utils/date";

type CreateTaskInput = {
  title: string;
  dueDate: string;
};

export async function createTaskWithNotification(input: CreateTaskInput) {
  const task = await createTask({
    title: input.title,
    dueDate: input.dueDate,
    createdAt: formatDate(new Date()),
  });

  // タスク作成後に担当者へ通知
  await sendNotification({
    message: `タスク「${task.title}」が作成されました`,
  });

  return task;
}

Service(外部通信層)― 外部世界との窓口

配置: src/services/

実際の fetch やDB操作を書く層です。外部APIやDBの仕様に依存する部分をここに集約することで、仕様変更があった際の修正箇所を限定できます。

やらないこと

  • UIロジックを書かない
  • 複数のServiceをまたいだ処理を書かない(それはUseCaseの役割)
// src/services/taskService.ts
import { db } from "@/lib/db";
import { Task } from "@/types/task";

export async function getAllTasks(): Promise<Task[]> {
  return db.task.findMany({ orderBy: { createdAt: "desc" } });
}

export async function createTask(data: Omit<Task, "id">): Promise<Task> {
  return db.task.create({ data });
}
// src/services/notificationService.ts
export async function sendNotification({ message }: { message: string }) {
  await fetch("https://api.example.com/notify", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${process.env.NOTIFY_API_KEY}`,
    },
    body: JSON.stringify({ message }),
  });
}

まとめ

各層の責務を一覧にまとめます。

配置 主な責務
Component components/ 表示・ユーザー操作
Hook hooks/ 状態管理・API呼び出し
API Route Handler app/api/ サーバの入口・秘密情報の保護
UseCase usecases/ 複数Serviceの組み合わせ(省略可)
Service services/ 外部APIやDBとの通信

「このコードはどこに書くべきか」と迷ったときは、上の表に立ち返るようにしています。層をまたいだ処理を書きたくなったときが、設計を見直すサインです。

← トップページに戻る