18
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsフルスタック構成の限界:開発サーバーが起動しなくなるまでの失敗談

Last updated at Posted at 2026-01-25

はじめに

私が今関わるプロジェクトではNext.jsを採用し、AIを活用したWebアプリケーションを開発しています。
しかし開発が進み、機能やチームメンバーが増えるにつれてコードの複雑性が増大。
最終的には npm run dev コマンドでローカル開発環境が起動しなくなる(メモリ不足・タイムアウト) という事態に陥りました。

なぜここまで開発環境が悪化してしまったのか。その原因と、私たちが痛感したアーキテクチャ上の反省点を共有します。

-- 構成 --

  • フレームワーク: Next.js 14 (App Router)
  • 言語: TypeScript, Node.js v20
  • UI: TailwindCSS, Shadcn/ui
  • Form & Validation: React Hook Form, Zod
  • データベース: PostgreSQL, Prisma ORM
  • 認証: NextAuth.js
  • テスト: Jest

1. Atomicデザイン採用の弊害

アトミックデザインはコンポーネントを小さな単位に分割し、再利用性を高める設計手法です。
プロジェクトの走り出し当初は綺麗に設計されたコンポーネントが配置されていました。
しかし、実際には過度に細分化されたコンポーネントが増え、管理が難しくなりました。
機能単位でのpageコンポーネントのコードが増え、最適なコンポーネント分割がされなくなりました。
さらに、アトミックデザインで詳細にコンポーネントが分割される想定だったため、フックを活用したロジックとUI層の分離をしていませんでした。

メンバーが増え開発が進むにつれ、コンポーネント分割されなくなり、pageコンポーネントにソースコードが集約され、さらにはロジック層とUI層の分離がされなくなりました。
機能単位でのコロケーション構成にし、UIとロジックを分離するContainer/Presentationalパターンを採用すべきでした。


【解説】ディレクトリ迷路と肥大化したPage

本来はAtomic Designの階層に分かれるはずが、探すのが面倒になり、結局 app/features/xxx/page.tsx にすべてを書いてしまう現象です。

理想

src/components/
├── atoms/
│   └── Button/
├── molecules/
│   └── SearchForm/
└── organisms/
    └── UserCard/

現実(「神」コンポーネント爆誕)

src/components/
└── pages/
    └── users.tsx  ← ここにUI、ロジック、スタイル、全てが集約

かろうじてatomsだけ再利用される。

※参考記事

2. API RoutesとServer Actionsの混在

Next.jsではAPI RoutesとServer Actionsの両方を使用できますが、後述するPrismaでのDBアクセス層がそれぞれに混在されており、
どのような意図で使い分けているのかチーム内で統一されていませんでした。
プロジェクト当初は Server Actionsが出始めだったため(Next.js 14)、試験的に 各機能単位でのpage.tsxから呼び出すデータ取得関数としてそれぞれ実装されていました。
しかし、機能が増えるにつれ、Server Actionsは使われなくなり、API RoutesでのGETをクライアントコンポーネントからFetchで呼び出すコードが混在し、コードの一貫性が失われました。

【コード例】同じ「ユーザー取得」なのにバラバラな実装
初期実装されたServer Actionsが無視され、検索機能のためにAPI Routeが勝手に生やされた例です。

1. 初期実装:ユーザー一覧(Server Actions派) サーバーコンポーネントから直接DB取得関数を呼ぶ、Next.js推奨の形で作られました。
// 📁 actions/user.ts
"use server";
import { prisma } from "@/lib/prisma";

// ユーザー一覧取得(初期表示用)
export async function getUsers() {
  // Prismaを直接呼んでいる
  return await prisma.user.findMany({
    orderBy: { createdAt: "desc" },
    select: { id: true, name: true, email: true } // 必要なフィールドを指定
  });
}

// 📁 app/users/page.tsx (Server Component)
import { getUsers } from "@/actions/user";

export default async function UserListPage() {
  const users = await getUsers(); // ここではServer Actionsを使用
  return (
    <div>
      <h1>ユーザー一覧</h1>
      {/* ... */}
    </div>
  );
}
2. 追加実装:ユーザー検索(Client Fetch / API Route派) 後から参画したメンバーが検索機能を実装。既存の getUsers を拡張したり再利用するのではなく、使い慣れた fetch を使うために専用のAPI Routeを作ってしまいました。
// 📁 app/api/users/search/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

// 検索用API(actions/user.tsと同じようなロジックを再実装...)
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const q = searchParams.get("q");

  // ❌ ロジックの重複!
  // selectするフィールドも微妙に actions/user.ts とズレていたりする
  const users = await prisma.user.findMany({
    where: { name: { contains: q } },
    select: { id: true, name: true } // emailが含まれていない等の不整合が発生
  });

  return NextResponse.json(users);
}


// 📁 components/molecules/searchForm.tsx (Client Component)
"use client";

export default function UserSearch() {
  // ...
  const handleSearch = async (query) => {
    // 既存のServer Actionsを使わず、わざわざAPIをfetchする
    const res = await fetch(`/api/users/search?q=${query}`);
    const data = await res.json();
    setUsers(data);
  };
  // ...
}

3. バックエンド層のベタ書き

いわゆるクリーンアーキテクチャやレイヤードアーキテクチャのような層構造を意識した設計がされておらず、API RoutesやServer Actions内に直接PrismaのDBアクセスコードがベタ書きされていました。
そのため、ビジネスロジックとデータベースアクセスロジックが混在し、コードの可読性と保守性が低下しました。

【解説】層のないアーキテクチャ
「Controller (Handler)」の中に全てが詰め込まれており、Service層やRepository層が存在しません。

[API Route / Server Action]
   │
   ├─ 入力バリデーション
   ├─ ビジネスロジック (if文の嵐)
   └─ DBアクセス (prisma.findMany...)
      (すべてが1つの関数内にある)

4. 過度なテストコード網羅

プロジェクトリリース当初は、page.tsx, API Routes, Server Actions, さらにクライアントコンポーネントすべてにレンダリングテストが書かれていました。
機能が追加されたり、変更が加わるたびにテストコードの修正が間に合わず、スキップされることが増えました。
その結果、破綻したテストコードが大量にできてしまいました。

npm run devサーバーが重くなってしまう。

これらの問題が重なり、バンドルサイズが肥大化しnpm run devコマンドでのローカル環境起動ができなくなり、開発効率が大幅に低下しました。

現在

これらの技術的負債を抱え、まずはAPI Routes, Server Actions, Prismaで作成されたバックエンドを分離する作業をしている最中です。

フロントエンドをどのようにすべきかはまだ確定していませんが、以下のような案がでています。 

  • BFFとしてNext.jsを採用
  • ReactでSPA化
  • Svelte kitのようなシンプルで破綻しづらいフレームワークを採用する

私なりの結論

中規模以上のチーム開発においてはNext.jsにはバックエンドを実装すべきではない。(プロジェクトの規模やフェーズにもよるのでNext.jsでどこまで実装するかはケースバイケース)
FastAPIやGo、RailsなどでRest APIサーバーを分離しておくべき。
TypeScriptで統一したいならHonoがよさそう?
Next.jsはBFFとしての役割に徹する。

この記事に大変共感しました。↓

そもそもNext.jsを使わないという選択肢もあります。

最後に

Next.jsは今なにかとHotなフレームワークです。
カスタマイズ性が高く、うまく使いこなせばモダンなWebアプリケーションをスピード感を持って開発できます。
一方その自由度が仇となり、チーム内で認識を共有しないと秩序のないコードベースになってしまいがちです。

RSCやNode.js自体にも脆弱性が見つかっています。
そもそもバックエンドをTypeScriptで書くべきではないという意見もあります。
正解は一つではないですが、この失敗談が誰かの技術選定に役立てば幸いです。

18
11
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?