この記事は約 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.ts は prisma を 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.ts の experimental.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 — 公式プロダクトページ