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