はじめに
「最初はControllerとService1個ずつでいいと思ってた」
NestJSで個人開発してたとき、最初は小さな機能だったから特に設計も気にせず進めてた。
だけど、ユースケースが増えるたびにモジュールが肥大化していき、
気づけば「このモジュール、誰も触りたくないやつ」になってた。
この記事では、そんな モジュール肥大化で詰んだ実体験と、
それを どうリファクタリングして乗り越えたかをまとめておきます。
🧩 最初の構成(小さく始めたつもりだった)
src/
└── user/
├── user.module.ts
├── user.controller.ts
├── user.service.ts
└── user.entity.ts
💥 詰んだ構成
最初はこんな感じだった。
-
Controller
にAPI10個以上 -
Service
にロジックどんどん生やす -
Entity
をそのままレスポンスに使ってた
正直これで快適だった。が……。
💣 詰んだ瞬間
-
user.service.ts
が 500行超え -
createUser()
が 分岐だらけ(招待コードあり/なし、キャンペーン経由など) - Entityをそのまま返してたせいで、Prismaの構造が外にモロバレ
- テストしようとしたら、DB依存が強すぎて単体化できない
- フロントの仕様変更がくるたびに、コードの影響範囲が爆発
🛠️ モジュール分割でやったこと
✅ ユースケースごとに Service を分けた
src/
└── user/
├── use-case/
│ ├── create-user-with-invite.service.ts
│ ├── sync-user-profile.service.ts
│ └── ...
→ 「1ファイル=1目的」にしたら、思考も整理されてテストもしやすくなった。
✅ DTO と Entity を完全に分離
-
user.entity.ts
は Prismaモデルに専念 -
dto/
フォルダを作って、CreateUserDto
,UserResponseDto
を分離 - クライアントに返すレスポンスも 明示的に型定義できるようになった
✅ Mapper を導入して変換処理を明確化
// user.mapper.ts
export class UserMapper {
static toDto(entity: UserEntity): UserResponseDto {
return {
id: entity.id,
name: entity.name,
email: entity.email,
// ...
};
}
}
→ これで「どこで Entity → DTO になるか」が一目で分かるように。
📐 リファクタ後の構成(抜粋)
src/
└── modules/
└── user/
├── user.module.ts
├── api/
│ ├── user.controller.ts
│ └── dtos/
├── use-case/
│ ├── create-user-with-invite.service.ts
│ └── sync-profile.service.ts
├── core/
│ ├── entities/
│ ├── repositories/
│ └── mappers/
-
Controller
は API専用 -
UseCase層
に処理を集約 -
Domainっぽい形
で整理が効くように
🔁 やって良かったこと/やりすぎたこと
✅ やって良かった
取り組み | 理由 |
---|---|
ユースケースごとにService分割 | テスト・リーダブル・拡張性全部向上 |
DTOとEntityの完全分離 | Prismaに引きずられないAPI設計ができた |
Mapper導入 | データ変換の責任範囲が明確になった |
⚠ やりすぎたかも
取り組み | 理由 |
---|---|
Serviceが細かくなりすぎた | ファイル数が爆増。探すのが手間(命名でカバーした) |
Mapperに処理詰めすぎた | 共通化しすぎて逆に再利用性が落ちた場面もあった |
✅ まとめ
- NestJSは構造化しやすい反面、モジュールを育てすぎると破綻する
- 特に個人開発だと、最初は雑に始めがち
- でも、後から整理しても全然間に合う
- モジュールが大きくなったら、**「ユースケース単位でServiceを切る」**のが第一歩
🙌 最後に
設計って「最初から完璧」にできることはほぼない。
でも、詰んだ時に構成を見直せるかどうかで、その後の開発体験が全然変わる。
自分みたいに「やばい、Service肥大しすぎた…」ってなった人の参考になればうれしいです。