【Claude Code/Cursor】2年運用したソフトウェアを2ヶ月でフルリプレースした話【プロンプトあり】
こんにちは、nocall株式会社CTOの森本です
みなさん、バイブコーディング楽しんでますか?
私は普段nocall.aiと言う電話AIエージェントの開発に携わっており、日々AIエージェントをフル活用して開発しています。
その中で、今回Claude CodeとCursorをフル活用して、2年運用したソフトウェアを2ヶ月でフルリプレース(作り替え)をしたので、その開発の中で得た知見を共有します。
実際にバイブコーディングしたダッシュボード👇

この記事でわかること
- バイブコーディングをプロダクションコードに落とし込むための設計と運用
- 仕様駆動開発とガードレール(AGENTS.md / spec.md)の使い分け
- AI駆動開発を安定させるために効いたフレームワーク選定
- 旧システムからのフルリプレースで得た改善効果
対象読者
- バイブコーディングをしている/したいエンジニア
- AIコーディングの進め方に悩んでいる人
はじめに:バイブコーディング、こんな悩みありませんか?
「AIにコードを書かせてみたけど、出力がバラバラで使い物にならない」
「レビューが追いつかない。AIが書いたコードのどこを見ればいいか分からない」
「テストがないまま進んで、あとで痛い目を見た」
私たちも同じ悩みを抱えていました。Claude CodeやCursorを使い始めた当初、AIの出力は不安定で、プロダクションコードとして使うには課題が多いと感じていました。
解決策:AIに「制約」を与える
バイブコーディングを安定させるために、AIに大きな制約を設けました。自由に書かせるのではなく、「ここに、こう書け」というルールを明確にしたのです。
具体的には2つのアプローチを採用しました。
- DDD・オニオンアーキテクチャを採用
- 仕様駆動開発を導入
1. DDD・オニオンアーキテクチャを採用
DDD/オニオンアーキテクチャについてはこの記事を読んでください
DDD/オニオンアーキテクチャを選んだのは、AIとの相性が良いからです。
役割と置き場所が明確
ドメイン層、ユースケース層、インフラ層と分かれていれば、AIは「どこに何を書くか」を迷いません。「このロジックはどこに置けばいい?」という曖昧さがなくなります。
依存方向が固定
内側から外側への一方向の依存ルールがあるため、変な結合が起きにくくなります。AIが勝手にインフラ層のコードをドメイン層に持ち込むような事故が防げます。
テストが書きやすい
ユースケース層はAPIのルートやDBに依存しないため、テストが書きやすくなります。DB/ORMのモックを用意する必要がなく、純粋なロジックのテストに集中できます。
2.仕様駆動開発(SDD)を導入
仕様駆動開発(Spec-Driven Developments(SDD))とは、
一言で言うと、仕様ドキュメントを作成し、その仕様に基づいてコーディングを行う手法です。
- 既存システムから仕様をspec.md書き出す
- 新しい仕様を壁打ちしながら仕様を詰める
- AIが実装
- コードがをレビューする
と言う流れで実装していきます

参考:
spec.mdに記述した内容
spec.mdには、そのソフトウェアの要件を定義していきます。
- ビジネス要件(料金や対象顧客など)
- ドメインモデル
- 振る舞い(ユースケース)
具体的には以下のようにAIと壁打ちして、AIにspec.mdを書かせていきました。
「2階層構造でリソース管理したい。1階層目は会社を表し、2階層目は部署やプロジェクト単位で使用する。ユーザーは1階層目に紐づくと思う。その他のリソースは2階層目に紐付くイメージ」
↓
🤖「WorkspaceとProjectに分けるのはどうでしょう?」
↓
「いいね。Workspaceは、契約中かどうかわかるようにしたい。契約関係上、契約が終了したら顧客データを全て抹消する必要があるので、Workspaceに紐づくデータは全て物理削除して。」
↓
🤖「おk」
最終的に一つの仕様書(spec.md)が完成🎉
仕様書の精度が品質を分けるので、ここには時間をかけてよいです。あとは「spec.mdに従って実装して」と指示するだけです。
とはいえ実装中に想定と違ったり、「やっぱり別の方法のほうがいいのでは?」と考えが変わるため、割とマイクロマネジメント的にコーディングエージェントを見ていました。
spec.mdの型を載せておきます。参考にしてください🙏
spec.md(サンプル)
# DDD仕様書テンプレート
## 1. 概要
* アーキテクチャ: **オニオンアーキテクチャ**(Domain中心)。
* 開発プロセス: **TDD(テストファースト)**を徹底。ユースケースごとに失敗テスト→最小実装→リファクタ。
* 本仕様は、データモデル/ドメイン振る舞い/ワークフロー/課金と通知を宣言的に定義する。
---
## 2. ビジネス要件
* **課金**
* (プロジェクト固有の課金体系を記述)
* **制限値**
* (リソースの上限、更新間隔などを記述)
* **アカウント管理**
* パスワード変更時は既存の他セッションを失効させ、セキュリティを担保する。
---
## 3. ドメインモデル概要
### 3.1 ドメインサイクル
ドメインの主要なライフサイクルを記述する。例:
- **準備フェーズ**: リソースの作成と設定
- **実行フェーズ**: メイン処理の実行
- **事後処理フェーズ**: 結果の分析と通知
- **フィードバックフェーズ**: 継続的改善
### 3.2 サブドメインと境界
ビジネスドメインを以下のように分割する:
- **テナント管理**: マルチテナント境界を定義し、リソースアクセス権を決定
- **プロジェクト運用**: 業務単位を確立し、外部連携を管理
- **コアドメイン**: (プロジェクト固有のコアビジネスロジック)
- **顧客データ管理**: 顧客属性のスキーマ管理
- **タスク管理**: 状態遷移を制御
- **実績管理**: 実績の唯一の真実と分析結果を保持
- **課金・メトリクス**: コスト算出
### 3.3 アグリゲート関係図
Workspace(テナント)
├── User(ユーザー)
│ └── WorkspaceMember(メンバーシップ)
└── Project(プロジェクト)
├── ProjectMember
├── ApiKey
├── CoreEntity(コアエンティティ)
│ ├── Setting1
│ ├── Setting2
│ └── Webhook
├── CustomerGroup
│ └── Customer
├── Task
├── ActivityLog
│ ├── Detail
│ └── Analysis
└── NotificationLog
### 3.4 アグリゲートと主要な振る舞い
各アグリゲートの責務を明確に定義する:
- **Workspace**: テナント境界。メンバー招待、状態遷移(招待中→有効→停止)を管理
- **User**: サインイン主体。メールアドレスを主キーとし、複数 Workspace/Project へ参加可能
- **Project**: 業務単位。Workspace 内で名前の一意性を担保
- **ApiKey**: 外部連携用の資格情報。`lastUsedAt` で最終利用時刻を追跡し、`revokedAt` で段階的無効化
### 3.5 ドメインサービスとポリシー
- **外部サービス連携**: 外部APIとの連携を担う
- **通知サービス**: メール/Webhook に従い通知を実行
- **ストレージサービス**: ファイルストレージを抽象化
---
## 4. オニオンアーキテクチャ
### 4.1 各層の責務
* **Domain(中心)**: エンティティ/値オブジェクト/集約/ドメインサービス。外部への依存なし。
* **Application**: ユースケース(入力ポート)とリポジトリインタフェースの定義。トランザクション境界を管理。
* **Infrastructure**: 永続化(Drizzle ORM)、外部サービスのアダプタ。リポジトリ実装もここに配置。
* **Presentation**: APIルート、ミドルウェア、認証設定。
### 4.2 依存の方向
Presentation層
↓
Infrastructure層 → Application層
↓ ↓
└────→ Domain層 ←─┘
**重要**: 依存は常に内側(Domain層)に向かう。
---
## 5. DDD: 集約と振る舞いの設計パターン
### 5.1 エンティティの設計パターン
// 生成: ファクトリメソッドでデフォルト値をマージ
Entity.create(params)
// 更新: 整合性チェック付きで一括更新
entity.update(params)
// アーカイブ/復元: ソフトデリート
entity.archive()
entity.restore()
// 永続化: インフラ層とのデータ交換
Entity.fromPersistence(data)
entity.toPersistence()
### 5.2 値オブジェクトの設計パターン
// 正規化: 入力文字列をクレンジング
ValueObject.parse(input)
// 派生値: 利用側から変換ロジックを排除
valueObject.formatted
valueObject.normalized
// 例外: 不正入力に対して HTTPException を送出
### 5.3 状態機械の設計パターン
// 状態遷移: pending → queued → executed|skipped|canceled
task.queue() // pending → queued
task.execute() // queued → executed
task.skip() // → skipped(完了済みへの遷移は禁止)
task.cancel() // → canceled(完了済みへの遷移は禁止)
### 5.4 ApiKey の設計パターン
* **生成**: `sk-{uuid}` 形式の秘密鍵を発行
* **検証**: 名前は必須・100 文字以内
* **操作**: `verifySecret()`, `markUsed()`, `revoke()`
* **状態**: `revokedAt` が設定されているものは利用不可
---
## 6. API ルート構造
### 6.1 URL 階層構造の設計パターン
/api
├── /health # ヘルスチェック
├── /shared-resources # 共有リソース
├── /external-callbacks # 外部サービスコールバック
├── /cron # 定期実行ジョブ
└── /workspace/:workspaceId
├── /account # アカウント設定
└── /project/:projectId
├── /core-entities # コアエンティティ CRUD
├── /customers # 顧客管理
├── /tasks # タスク管理
├── /activity-logs # アクティビティログ
├── /api-keys # APIキー管理
└── /webhooks # Webhook管理
### 6.2 セキュリティ設計の原則
* **セッションに状態を持たない**: リソースIDはURLパラメータから取得
* **リクエストごとの検証**: 各リクエストでメンバーシップを検証
* **階層関係の検証**: リソースが指定された親に属しているかを検証
* **情報リークの防止**: 権限がない場合も 404 を返す
* **APIキー認証**: 公開APIはAPIキー認証必須
AGENTS.md に記述した内容
spec.mdにはソフトウェアの仕様を記述したので、AGENTS.mdには以下の内容を記述しました。「毎回絶対に覚えておいてほしいこと。これがないと秩序が乱れること」を中心にAIに渡していました。
| 項目 | 内容 |
|---|---|
| 技術スタック | 使用言語、フレームワーク、ライブラリ |
| ファイル構造の意味 | 各ディレクトリが何のためにあるか |
| アーキテクチャの説明 | レイヤー構成と依存ルール |
| 開発ワークフロー | ブランチ戦略、TDDの開発手法 |
| コーディング規約 | 命名規則、フォーマット、ライブラリの使用方法 |
| 仕様更新ルール | 「仕様が変わったらspec.mdを更新する」という指示 |
逆にAGENTS.md書くべきではないこと
特定の場面のみに適用する知識は書かない方が良いです。AIが自ら調べられる動線を提示するだけでOKです。最近だと、Skillsを使うのも手ですね。
AGENTS.mdの型を載せておきます。参考にしてください🙏
AGENTS.md
アプリケーションに依存しているところは省略しています。
# Agent Handbook
## 1. 概要
(省略)
### 仕様書の更新方針
- ユーザーの指示によって仕様が変わった場合は、`docs/spec.md` を更新すること
- 既存のコードと仕様書が一致しない場合は、実装に合わせて仕様書を修正すること
- 仕様に矛盾がある場合は、ユーザーに確認した上で仕様書を更新すること
- 仕様書に情報が不足している場合は、必要な情報を追記すること
## 2. コア機能
(省略)
## 3. 技術スタック
- Turborepo + pnpmでモノレポ管理
- Biome.jsで lint / format 一元化
- Vitestでユニット/コンポーネントテスト
- Next.js 15 (App Router with Hono API routes)
- Hono APIハンドラ
- Better Auth
- React 19 + TypeScript 5
- Tailwind CSS 4 / shadcn/ui / Lucide Icons
- SWR, React Hook Form
- Drizzle ORM + Supabase Postgres
- Zodによるバリデーションと型共有
## 4. ワークスペース構成
このプロジェクトは**オニオンアーキテクチャ**を採用しています。
my-project/
├── apps/
│ ├── frontend/ # Next.js(UI層)
│ │ ├── app/ # Next.js App Router
│ │ │ ├── (auth)/ # 認証関連ページ
│ │ │ └── dashboard/ # ダッシュボード関連ページ
│ │ ├── components/ # Reactコンポーネント
│ │ │ └── ui/ # shadcn/ui コンポーネント
│ │ ├── hooks/ # React Hooks
│ │ └── lib/ # フロント専用ユーティリティ
│ │ └── client.ts # Hono RPC クライアント
│ │
│ └── backend/ # Hono API + DDD
│ ├── domain/ # ドメイン層(中心、依存なし)
│ │ ├── user/
│ │ │ ├── __test__/ # ドメインエンティティのテスト
│ │ │ │ └── user.test.ts
│ │ │ └── user.ts # エンティティ
│ │ ├── order/
│ │ └── ... # その他のドメインエンティティ
│ │
│ ├── use-cases/ # アプリケーション層(ドメイン層に依存)
│ │ ├── user/
│ │ │ ├── __test__/ # ユースケースのテスト
│ │ │ │ └── create-user.test.ts
│ │ │ └── create-user.ts
│ │ └── ... # その他のユースケース
│ │
│ ├── infrastructure/ # インフラ層(外側、インターフェースを実装)
│ │ ├── repositories/
│ │ │ ├── interfaces/ # リポジトリインターフェース(抽象)
│ │ │ │ └── user-repository.ts
│ │ │ └── drizzle-user-repository.ts # リポジトリの実装
│ │ └── external/ # 外部サービス連携
│ │ └── email-service.ts
│ │
│ ├── presentation/ # プレゼンテーション層
│ │ ├── routes/ # APIルート定義
│ │ │ └── users.ts
│ │ ├── middleware/ # 認証・アクセス制御
│ │ ├── auth/ # 認証設定
│ │ └── index.ts # API統合・グローバルエラーハンドラー
│ │
│ └── lib/ # 共有ユーティリティ
│ └── schemas/ # Zodスキーマ(API/フロント共有)
│
└── packages/
├── config/ # Biome, TS, Tailwind 等 preset
├── types/ # 型定義の共有パッケージ
└── database/ # Drizzle schema, migrations, client
### 各層の責務
#### ドメイン層 (`domain/`)
- **責務**: ビジネスロジック、バリデーション
- **含むもの**: エンティティ、値オブジェクト、ドメインサービス
#### アプリケーション層 (`use-cases/`)
- **責務**: ユースケースの実行フロー、トランザクション管理
- **依存**: ドメイン層のみ(インターフェース経由)
#### インフラ層 (`infrastructure/`)
- **責務**: 外部システムとの連携
- **含むもの**: リポジトリ実装(Drizzle ORM)、外部サービス
#### プレゼンテーション層 (`presentation/`)
- **責務**: HTTP リクエスト/レスポンス処理
- **含むもの**: APIルート
### 依存性の方向
プレゼンテーション層
↓
インフラ層 → アプリケーション層
↓ ↓
└─→ ドメイン層 ←─┘
**重要**: 依存は常に内側(ドメイン層)に向かう。外側の層は内側の層を知っているが、内側の層は外側の層を知らない。
## 5. 開発ワークフロー
- **TDD推奨**: テストを記述してからコーディングする
1. テスト観点の表を作成(正常系・異常系・境界値)
2. テストコードを実装
3. 本体のコードを実装してテストをパスさせる
- コードの編集 → `pnpm type-check` → `pnpm lint:fix` → `pnpm test`で動作確認し、不必要なエラーが消えるまで繰り返す(ただし2回チャレンジしてダメなら元に戻してユーザーに問題箇所を提示する)
- ユーザーの操作が必要な場合はユーザーに依頼する
- 使用方法がわからないライブラリがあった場合、検索をして使用方法を確認する。
## 6. コーディング規約
- PascalCase (components/types), camelCase (values/hooks), kebab-case (directories, files), UPPER_SNAKE_CASE (定数) を徹底。
- 関数型・宣言的スタイルを優先する。
- エラーメッセージは日本語を使用する。
- クライアントのAPIコールにはuseSWRを使用する。
- **shadcn/uiの使用**:
- shadcn/uiのコンポーネントを積極的に使用すること
- コンポーネントが必要な場合は、まず `components/ui/` ディレクトリを確認すること
- 既存のコンポーネントがない場合は、[shadcn/ui LLM context](https://ui.shadcn.com/llms.txt) を参照してコンポーネントを検索し、必要に応じてインストールすること
- コンポーネントの検索や使用方法がわからない場合は、`https://ui.shadcn.com/llms.txt` を活用すること
- Tailwindは意味の近いユーティリティをグルーピングし、`@/` import aliasでパスを統一。
- サーバーユーティリティは副作用を限定し、単体テスト可能な形で実装。
- TypeScriptは strict モード、Zodで入出力を検証、`any` は絶対に使わないこと。
- **動的importは使用しないこと**: `import()` による動的インポートは禁止。通常の `import` 文を使用する。
- 環境変数(env)は必ず専用のファイル経由で参照すること。
- **Zodスキーマの共有**:
- APIとフロントエンドで共有するZodスキーマは `lib/schemas/` に配置する
- APIルートでは `zValidator` で使用
- フロントエンドでは同じスキーマを `react-hook-form` の `zodResolver` で使用してバリデーションを共有
- Hono RPC は `client` を介して呼び出し、ルート定義は `new Hono().use(...).get(...).post(...)` のようにメソッドチェーンで記述して `AppType` の推論を維持する。
- **Honoコンテキスト変数の取得**:
- ミドルウェアを通過した後は `c.var.{変数名}` を使用する(例: `c.var.clientId`, `c.var.activeWorkspaceId`, `c.var.projectId`)
- ルートハンドラーでは常に `c.var` を使用すること
- **APIレスポンス形式**:
- レスポンスは直接データを返し、ステータスコードは明示的に指定する。(例: `c.json(voices, 200);` )
- **エラーハンドリング**:
- ルートハンドラーでの `try-catch` は原則不要
- グローバルの `onError` ハンドラーがエラーをキャッチ
- **エラーメッセージの規約**:
- ドメイン層・ユースケース層・ルートハンドラーでのエラーは `HTTPException` を使用してステータスコードを明示する
- エラーメッセージは「フィールド名(日本語)+ 説明(日本語)」の形式にする
- 例: `throw new HTTPException(400, { message: 'イベントの時間は0分より大きくなければなりません' });`
- これによりAPIレスポンスを見た時に、どのフィールドでエラーが発生したのかが明確になる
- **トーストの使用**:
- ユーザーへの通知には `react-hot-toast` を使用する
- サーバーからのエラーメッセージを必ず表示すること
- エラーハンドリング時は、レスポンスから `errorData.error` を取得してトーストに表示する
import { client } from '@/lib/api/client';
const response = await client.api.agents.$get();
const { data } = await response.json();
console.log(data);
### Honoコンテキスト変数の使用例
.get('/project/:projectId', projectAccessMiddleware, async (c) => {
const projectId = c.var.projectId; // 型安全
const clientId = c.var.clientId; // 型安全
// ...
});
### APIレスポンスの例
// 良い例: 直接データを返す
.get('/agents', async (c) => {
const agents = await getAgents();
return c.json(agents, 200);
});
### エラーハンドリングの例
// 良い例: グローバルエラーハンドラーに任せる
.post('/agents', async (c) => {
const result = await createAgent();
return c.json(result, 201);
// エラーは自動的にグローバルの onError でキャッチされる
});
### ドメイン層でのエラーの例
import { HTTPException } from 'hono/http-exception';
export class AgentCalendarSettings {
private validate(): void {
if (this.eventDurationMinutes <= 0) {
throw new HTTPException(400, {
message: 'イベントの長さは0より大きくなければなりません',
});
}
}
}
使用してよかったフレームワーク
最後に、使用してよかったフレームワークを紹介します。
以下のフレームワークにより堅牢なコーディングができるようになったと実感しています。
hono
- 軽量
- 型安全なmiddlewareやroute機能
- monorepo採用なので、RPCでデータ型を共有でき、フロント←→バック間のAPI齟齬がない
// メソッドチェーンで記述することでHono RPCが有効になり、型が効く
const app = new Hono()
.get('/:userId', async (c) => {
const userId = c.req.param('userId');
const user = await getUser(userId);
return c.json(user, 200);
});
export type AppType = typeof app;
export default app;
// クライアント側からの型安全なAPI呼び出しが可能
// 仮にAIがミスしても、型エラーで気づける
const client = hc<AppType>('/api');
const response = await client.users.$get();
const users = await response.json();
// パラメータ付き
const response = await client.users[':userId'].$get({
param: { userId: '123' },
});
その他
- useSWR
- better-auth
- drizzle
- shadcn
- zod
- biome
- vitest
旧システム → 新システムで良くなったこと
-
開発速度が速くなった。2倍くらい
指標 新→旧 変化 コミット/日 +83% 追加行数/日 +107% PR/日 +122% -
テストが充実し、レビューの負荷が下がり、リリース時の心理的負荷が減った
-
一貫したアーキテクチャでAIの出力品質が安定した
-
型補完、monorepo、最新のフレームワークによって開発が快適
なぜフルリプレースをしたのか?
リセットしたかったから

nocallのサービスを2023年末に開始し、MVPのコードベースで2年運用してきました。コーディング規約もなく、設計思想も初期から大きく方針転換していたため技術負債が積み上がっていました。
ここ半年〜1年のコーディングエージェントの進化を見て「今ならいける!!!」と判断し、フルリプレースを決断しました。
採用情報
架電特化のAI電話SaaSを展開しているnocall.ai株式会社では、エンジニアを積極採用中です!
ジュニア層のエンジニアからリーダー職まで幅広く募集しています。
興味をもっていただいた方はカジュアル面談でぜひお話ししましょう!
Xもフォローしてください!
Discussion