はじめに
最近、AIによるコード生成がかなり実用的になってきました。
CRUD API程度であれば、AIに仕様を渡すだけでかなりのところまで実装してくれます。
一方で、実際の業務システムを開発していると、コードを書くことよりも難しいのが、
- DB設計
- 認可
- API設計
- 業務ルール
- ユースケース
- ドキュメント
- テスト
といったものを、どう一貫して管理するかです。
特に問題になるのが、同じ仕様が複数の場所に存在してしまうことです。
例えば、
仕様書
↓
OpenAPI
↓
TypeScript
↓
テスト
のそれぞれに似た情報を書いていると、少しずつ乖離していきます。
そこで今回は、
人間は「何を作るか」を定義し、AIとツールに「どう作るか」を任せる
という考え方で、バックエンド開発のアーキテクチャを考えてみました。
目指すアーキテクチャ
今回考えた構成は以下です。
人間が管理するSSOT
┌──────────────────────────────────────────────────┐
│ │
│ Requirements Use Cases │
│ 要件・機能一覧 業務仕様 │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ schema.zmodel business-api.yml │
│ データ・認可 HTTP API契約 │
│ │
└──────────────┬───────────────┬───────────────────┘
│ │
▼ ▼
┌─────────┐ ┌──────────────┐
│ZenStack │ │ Fastify │
└────┬────┘ └──────┬───────┘
│ │
│ Business API
│ │
│ ▼
│ UseCase
│ │
└────────────────┘
│
▼
PostgreSQL
AI / Code Generation
│
┌─────────────┼─────────────┐
▼ ▼ ▼
UseCase Tests Boilerplate
code
ポイントは、コードをSSOTにしないことです。
人間が管理するのは、
- 要件
- ユースケース
- データモデル
- API契約
です。
その情報をもとに、AIやコード生成ツールに実装を作ってもらいます。
1. まず「SSOT」を分割する
ここで重要なのが、
「SSOTを1個にする」
必要はないということです。
むしろ、異なる種類の情報を1つのファイルに詰め込むと破綻します。
そこで、それぞれの責務ごとにSSOTを持たせます。
| SSOT | 管理するもの |
|---|---|
| Requirements | システムとして何が必要か |
| Use Case | 業務上どう振る舞うか |
| ZModel | データ構造・リレーション・認可 |
| OpenAPI | HTTP APIの契約 |
| TypeScript | 実装 |
この考え方が今回のアーキテクチャの中心です。
2. データモデルはZModelをSSOTにする
DB設計にはZenStackのZModelを使います。
例えば、
model Project {
id String @id @default(cuid())
name String
status ProjectStatus
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members ProjectMember[]
}
ここには、
- テーブル
- カラム
- 型
- リレーション
- 制約
- 認可
など、データに関する情報を集約します。
DB設計書を別途手で作る必要はありません。
schema.zmodel
│
├── DB schema
├── ORM
├── CRUD API
└── Authorization
という形にできます。
3. CRUDは生成する
業務システムではCRUD APIが大量に発生します。
例えば、
GET /projects
POST /projects
GET /projects/:id
PATCH /projects/:id
DELETE /projects/:id
これを毎回手で実装するのは、かなりもったいない。
そこでZenStackに任せます。
schema.zmodel
↓
ZenStack
↓
CRUD API
これによって、CRUDの実装コストを大きく減らせます。
さらに、認可ルールもZModel側に定義できます。
つまり、
CRUD
認可
ORM
DB
という、比較的機械的な部分をツールに任せます。
4. 業務APIはCRUDと分離する
ここがかなり重要です。
例えば、
POST /projects/:id/approve
は単なるCRUDではありません。
「プロジェクトのstatusを書き換える」というDB操作だけではなく、
- 現在の状態を確認する
- 承認権限を確認する
- 承認者を記録する
- 承認日時を記録する
- 必要ならイベントを発生させる
といった業務処理があります。
これをCRUD APIに無理やり押し込むと、API設計がどんどん複雑になります。
そこで、
CRUD API
↓
ZenStack
Business API
↓
UseCase
と分離します。
5. Business APIはOpenAPIをSSOTにする
業務APIはOpenAPIで定義します。
paths:
/projects/{projectId}/approve:
post:
operationId: approveProject
parameters:
- name: projectId
in: path
required: true
schema:
type: string
responses:
'200':
description: Project approved
ここではHTTP APIについて、
- URL
- HTTP method
- Request
- Response
- Status Code
- operationId
を定義します。
重要なのは、OpenAPIに業務ロジックを書かないことです。
OpenAPIはあくまで、
「このシステムには、こういうAPIがあります」
という契約です。
6. FastifyをHTTP層にする
HTTPサーバーにはFastifyを使います。
Fastify
│
┌────────────┴────────────┐
│ │
ZenStack CRUD Business API
│ │
│ ▼
│ UseCase
│ │
└─────────────────────────┘
Fastifyをシステム全体のHTTPレイヤーとして使うことで、
- 認証
- Middleware / Hook
- HTTP logging
- Validation
- Routing
- Error handling
などを一箇所にまとめられます。
7. OpenAPIからBusiness APIを接続する
OpenAPIを手でFastifyのrouteに書き写すのも避けたいところです。
そこでOpenAPIからFastifyのrouteを構成します。
例えば、
operationId: approveProject
という定義から、
POST /projects/:projectId/approve
↓
approveProject
↓
ApproveProjectUseCase
という接続を作ります。
FastifyにはOpenAPIと連携するエコシステムがあり、OpenAPIを起点にrouteを構成することができます。
これによって、
OpenAPI
↓
Fastify route
↓
Handler
という流れを作れます。
8. UseCaseはコードにする
ここで少し意外な設計をします。
UseCase自体はSSOTにしません。
例えば、
export class ApproveProjectUseCase {
async execute(command: {
projectId: string;
approvedBy: string;
}) {
const project =
await db.project.findUnique({
where: {
id: command.projectId,
},
});
if (!project) {
throw new Error('Project not found');
}
if (project.status !== 'SUBMITTED') {
throw new Error('Project is not submitted');
}
return db.project.update({
where: {
id: command.projectId,
},
data: {
status: 'APPROVED',
approvedBy: command.approvedBy,
approvedAt: new Date(),
},
});
}
}
という実装になります。
一見すると、
UseCaseもドキュメントから自動生成すべきでは?
と思います。
しかし、ここは無理に自動化しません。
9. AIを「実装者」にする
ここでAIが登場します。
人間が、
UC-PROJECT-APPROVE.md
を整備します。
例えば、
# プロジェクト承認
## アクター
プロジェクト管理者
## 前提条件
- プロジェクトが申請済みである
- 操作者が承認権限を持っている
## 業務ルール
- 申請済み以外は承認できない
- 承認者を記録する
- 承認日時を記録する
## 結果
- プロジェクトが承認済みになる
そしてAIに、
このUseCase仕様を実装してください。
DBモデルはschema.zmodelを参照してください。
API契約はbusiness-api.ymlを参照してください。
既存の実装パターンに従ってください。
テストも作成してください。
と依頼します。
すると、
UseCase document
│
▼
AI
│
├── UseCase
├── Unit Test
└── Integration Test
を生成できます。
10. なぜUseCaseのコードをSSOTにしないのか
業務仕様と実装は別物だからです。
例えば、
「申請済みのプロジェクトだけ承認できる」
というルールが業務仕様です。
これを、
if (project.status !== 'SUBMITTED') {
throw new Error(...);
}
と書くのは実装方法です。
将来、
- TypeScriptから別の言語に変更する
- ORMを変更する
- アーキテクチャを変更する
- エラー処理を変更する
といったことがあっても、
「申請済みのプロジェクトだけ承認できる」
という業務ルールは変わりません。
したがって、
業務仕様をSSOTにして、実装は交換可能にする
という考え方です。
11. AIには「仕様を考えさせない」
ここも重要です。
AIに、
プロジェクト承認APIを作って
とお願いすると、AIが業務ルールを推測してしまいます。
これは危険です。
代わりに、
Requirements
+
UseCase document
+
schema.zmodel
+
business-api.yml
↓
AI
↓
Implementation
とします。
AIは、
「何を作るか」
ではなく、
「決められた仕様をどう実装するか」
を担当します。
12. テストもAIに生成させる
UseCase documentに、
## 業務ルール
- SUBMITTED のプロジェクトのみ承認できる
- DRAFT は承認できない
- REJECTED は承認できない
- 承認者を記録する
と書いてあれば、
AIに、
この業務ルールを網羅するテストを作成してください。
と依頼できます。
すると、
UseCase document
│
├──────────→ UseCase
│
└──────────→ Tests
となります。
この構造にすると、業務仕様とテストの対応関係も明確になります。
13. 最終的な開発フロー
この構成での開発フローは、
① 要件を整理
↓
② UseCaseを定義
↓
③ schema.zmodelを設計
↓
④ business-api.ymlを設計
↓
⑤ AIに実装させる
↓
⑥ AIにテストを書かせる
↓
⑦ 人間がレビュー
↓
⑧ CI
になります。
人間がコードを一行ずつ書くことが目的ではなく、
人間が仕様を正しく定義し、AIに実装を委譲する
ことを目的にしています。
14. それぞれのツールに「考えさせる範囲」を限定する
このアーキテクチャでは、各ツールの責務をかなり明確にしています。
Requirements
│
│ 「何が必要か」
▼
UseCase
│
│ 「どういう業務であるか」
▼
schema.zmodel
│
│ 「どんなデータ・権限か」
▼
business-api.yml
│
│ 「どういうHTTP APIか」
▼
AI
│
│ 「どう実装するか」
▼
TypeScript
この境界が重要です。
例えばAIにDB設計まで考えさせるのではなく、
schema.zmodel
を与えます。
API仕様までAIに考えさせるのではなく、
business-api.yml
を与えます。
業務ルールまでAIに考えさせるのではなく、
usecase.md
を与えます。
15. このアーキテクチャのメリット
ボイラープレートを減らせる
CRUDはZenStack。
HTTP routingはFastify/OpenAPI。
型定義やクライアントもOpenAPIから生成できます。
AIにはUseCase実装とテストを任せられます。
ドキュメントとコードの乖離を減らせる
人間が管理する仕様を明確にします。
UseCase
schema.zmodel
business-api.yml
が仕様の中心です。
コードは仕様から派生するものと考えます。
AIに仕事をさせやすい
AIに大量の既存コードを読ませて、
「なんとなくこのシステムっぽく実装して」
とするのではなく、
UseCase
+
Data Model
+
API Contract
+
Coding Convention
を入力として与えられます。
これはAIにとっても扱いやすい形式です。
16. この設計の本質
結局、このアーキテクチャでやりたいことは、
ソフトウェア開発における「人間が考える部分」と「機械が作業する部分」を分離する
ことです。
人間がやること:
要件を決める
業務ルールを決める
データモデルを決める
API契約を決める
機械にやらせること:
CRUD実装
HTTP routing
型生成
UseCase実装
テスト生成
API Client生成
という分担です。
17. 最終形
最終的には、こんな開発環境を目指します。
HUMAN
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
Requirements UseCase Data Model
│ schema.zmodel
│
│
▼
business-api.yml
│
┌───────────┴───────────┐
│ │
▼ ▼
ZenStack Fastify
│ │
│ Business API
│ │
│ ▼
│ AI
│ │
│ ▼
│ UseCase
│ │
└───────────┬───────────┘
│
▼
PostgreSQL
AI-generated
─────────────
UseCase
Tests
Client
Boilerplate
おわりに
AIによって「コードを書くコスト」が下がっていく一方で、これから重要になるのは、何を作るのかを機械に正しく伝えることだと思っています。
そのためには、AIにコードを書かせる前に、
- 業務仕様
- データモデル
- API契約
を明確な形式で管理する必要があります。
今回考えたアーキテクチャでは、
人間が仕様を管理し、ツールが機械的な部分を生成し、AIが実装を担当する
という役割分担を目指しました。
特定の業務システムに限らず、CRUDが多く、業務APIも存在する一般的なWebバックエンドであれば応用できるのではないかと思っています。
まだ実際の開発を通して検証している途中なので、特に
- UseCase documentをどこまで形式化するか
- AIにどこまで実装を任せるか
- OpenAPIとUseCaseの境界をどうするか
- 仕様変更時にどのSSOTを変更するか
については、今後さらに検討していきたいところです。