0
0

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 App Router で Client → service の value import が build を壊す罠 — src/config 分離で防ぐ

0
Posted at

この記事は約 5 分で読めます。

筆者プロフィール: ソフトウェアエンジニア。「知った気にならない。いつまでも学び続ける」を信条に、業務と個人開発の両輪で技術を磨いています。AI 駆動開発で複数の個人開発アプリを構築・運用中。
👉 ポートフォリオ: 筆者ホームページ

Client Component から service ファイルの 定数を 1 つ import しただけ で、Prisma が client bundle に混入して build が壊れる — Next.js App Router の典型的な罠です。本記事では、運用中の SaaS 「たすきば Knowledge Relay」 で採用した src/config/ 分離 での回避策を整理します。

サービスの機能紹介・画面イメージ・コンセプトは公式プロダクトページをご覧ください。
👉 たすきば Knowledge Relay — 公式プロダクトページ

何が起きたか

たすきばのプロジェクト編集画面で、文字数上限の表示を加えたかった。

// src/app/(dashboard)/projects/edit-dialog.tsx
'use client';
import { PROJECT_NAME_MAX_LENGTH } from '@/services/project.service';

export function ProjectEditDialog() {
  return (
    <Input maxLength={PROJECT_NAME_MAX_LENGTH} placeholder="プロジェクト名" />
  );
}

なんでもないコードに見えます。

しかし pnpm build で:

Error: Cannot find module '.prisma/client/default'
Module not found: Can't resolve 'fs' in '...'
PrismaClient is unable to be run in the browser.

巨大なエラーで build が落ちました


1. 原因 — Webpack の transitive 依存解析

Next.js (Webpack ベース) は、Client Component の bundle を作るとき、import した module をすべて辿ります。

edit-dialog.tsx ('use client')
  ↓ import
project.service.ts
  ↓ import
prisma (server-only)

project.service.ts 自体は server 側で動く想定ですが、その中で prisma を import している。Client Component が project.service.ts から 何か 1 つでも import すると、Webpack は依存関係を辿って prisma も client bundle に入れようとする

PrismaClient は Node.js の fs 等を使うため、ブラウザ環境では動かず、build が失敗 します。


2. 解決 — 定数は src/config/ に分離

業務的意味を持つ定数 (文字数上限・色・ルート等) は、src/config/ に集約します。

// src/config/validation.ts
export const PROJECT_NAME_MAX_LENGTH = 200;

src/config/validation.tsprisma を import しない。純粋な定数モジュール。Client Component からも安全に import できます。

// ✓ OK
'use client';
import { PROJECT_NAME_MAX_LENGTH } from '@/config/validation';

export function ProjectEditDialog() {
  return (
    <Input maxLength={PROJECT_NAME_MAX_LENGTH} placeholder="プロジェクト名" />
  );
}

build は通る


3. type-only import なら OK

「型だけ import すればいいのでは?」と思うかもしれません。

import type { ProjectSchema } from '@/services/project.service';  // type-only

これは OK。TypeScript の型は build 時に消えるため、Webpack の依存解析にも乗りません。

import 方式 安全性
import type { ... } ✅ 安全
import { ... } (value) ❌ prisma を巻き込む

「type と value を区別して import する」と運用すれば回避できますが、毎回意識するのは無理。そもそも service 側に定数を置かない ルールにします。


4. service ファイルから定数を export しないルール

たすきばは、Service 層の .service.ts ファイルから 定数を export しない ことを徹底。

// src/services/project.service.ts
// ✗ NG: 定数を export しない
// export const PROJECT_NAME_MAX_LENGTH = 200;

import { PROJECT_NAME_MAX_LENGTH } from '@/config/validation';  // config から import
// (使うだけ)

export async function createProject(...) { ... }
export async function updateProject(...) { ... }
ファイル 役割
src/config/ 定数のみ (prisma 非依存)
src/services/*.service.ts 関数のみ export (定数は config から import)

5. config/ は prisma を import しないルール

src/config/ 配下のファイルは、prisma / auth / DB 関連を 一切 import しない

// ✓ OK: src/config/validation.ts
export const PROJECT_NAME_MAX_LENGTH = 200;
export const KNOWLEDGE_TITLE_MAX_LENGTH = 100;

// ✗ NG: 以下のような import は禁止
// import { prisma } from '@/lib/db';
// import { auth } from '@/lib/auth';

これにより、Client Component / Server Component / Service 層のどこからも安全に import できます。

ESLint カスタムルールで src/config/ 配下の重い import を検出しています。


6. Server / Client Component の責務

この罠を踏まないために、Next.js App Router の Server / Client 境界を意識します。

種類 役割
Server Component (デフォルト) DB アクセス、認可、データフェッチ
Client Component ('use client') インタラクティブな UI、ユーザ入力、状態管理
// ✓ OK: Server Component で service を呼ぶ
export default async function Page() {
  const projects = await listProjects({ viewerTenantId, viewerUserId });
  return <ProjectList projects={projects} />;
}

// ✓ OK: Client Component で props を受ける
'use client';
export function ProjectList({ projects }: { projects: Project[] }) {
  return <ul>...</ul>;
}

Client は props 経由で受け取るのが原則


7. serverComponentsExternalPackages で回避する代替案

next.config.tsexperimental.serverComponentsExternalPackages を使う方法もあります。

const nextConfig: NextConfig = {
  experimental: {
    serverComponentsExternalPackages: ['@prisma/client'],
  },
};

@prisma/client を server-only に強制でき、Client Component から import しようとするとビルド時にエラーが出ます。

しかし、これは エラーを早期に出すだけ で、根本解決ではない。Client Component から service を import する欲求自体は残ります。

たすきばは、そもそも Client から service を import しない設計 に振り切りました。root cause solution として、config 分離が最善 です。


おわりに

対策
Client から service の定数 import で prisma 混入 定数は src/config/ に分離
service ファイルから定数 export 禁止 (config からのみ)
config から prisma を import 禁止
Server / Client の境界 Server で fetch、Client は props 受け

たすきばは、この境界を厳守することで、build 失敗を構造的に防いで います。

本記事の罠と回避策は、運用中の SaaS 「たすきば Knowledge Relay」 で実体験したものです。
👉 たすきば Knowledge Relay — 公式プロダクトページ

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?