この記事で伝えること
- 画面起点ではなくユースケース起点で要件を落とす方法
- 非機能要件(NFR)→意思決定→参照構成の一本道
- React / Vue / 純JS + Node(Fastify, Node 20)による薄い実装例
- 設計を残すADR(Architecture Decision Record)テンプレとレビュー・チェックリスト
1. 背景
「とりあえず画面とAPIを作る」から卒業する。上流で合意すべきは業務ユースケース・品質・コストであり、ここを言語化してから構成・コードへ降ろす。
本稿は、その逆算の“道筋”を示す。
2. 要件から非機能へ(NFR決定表の型)
業務要件(ビジネスフロー)をユースケースに分解し、NFRを定量化する。
観点 | 指標(例) | 目標値例 | 備考 |
---|---|---|---|
可用性 | 稼働率 | 99.9% | 業務時間帯のみ等も可 |
性能 | 95Pレスポンス | ≤ 300ms | 画面別に設定 |
スケール | 同時接続 | 2,000 | ピーク帯の根拠を明記 |
セキュリティ | 認可粒度 | 行レベル | PII取り扱い有無 |
運用 | 監査証跡 | 保持12カ月 | 参照UIの要否 |
コスト | 月額上限 | 35万円 | 変動幅と根拠 |
ポイント
- 目標値をユースケース単位で決める(画面横断の平均値は不可)
- 根拠(トラフィック仮説・利用率)を設計文書の同一箇所に残す
3. 意思決定を残す:ADRテンプレ(TypeScript)
「なぜそうしたか」を残さなければ、上流の判断は再現できない。
(通常md形式が主流ですが、私がよくTSで書いてるので、それを掲載)
// /docs/adr/0001-bff-and-api-composition.ts
export type ADR = {
id: string; date: string; title: string;
context: string; decision: string; alternatives: string[];
consequences: { positive: string[]; negative: string[] };
nfrImpact: { availability?: string; latency?: string; security?: string; cost?: string; };
};
export const ADR_0001: ADR = {
id: "ADR-0001",
date: "2025-07-29",
title: "BFF + API Composition を採用する",
context: "フロントごとにAPIの粒度要件が異なる。バックエンドは複数サービス。",
decision: "画面要件に近い粒度はBFFで合成。バックエンドは疎結合を維持。",
alternatives: ["バックエンド統合", "GraphQLの集中導入", "フロント直叩き"],
consequences: {
positive: ["UI変更の影響をBFFに閉じ込められる", "バックエンドの責務が明瞭"],
negative: ["BFFの運用・性能管理が必要", "二重キャッシュ設計の複雑化"]
},
nfrImpact: { latency: "BFFで集約→RTT削減", cost: "BFF運用コスト増を許容" }
};
4. 参照構成(論理図)
[Browser (React / Vue / JS)]
│ HTTPS
▼
[BFF (Node 20 + Fastify)]───[AuthZ/Cache(Redis)]
│
├── [Service A (REST)]
├── [Service B (REST)]
└── [Reporting / Query DB]
意図
- BFFでユースケース粒度へ合成、リトライ/タイムアウト/サーキットブレーカを集中
- 認可はBFFで最終判定(画面条件を考慮)
- 読み取り最適化(集計/二次インデックス)は別系統へ
5. ユースケース駆動の粒度設計(例)
医療系の案件を請け負うことが多いので、患者一覧を例にします。
UC-01:患者一覧の検索(複合条件・ページング)
- 入力:診療科、在院/退院、期間、フリーテキスト
- 出力:20件/ページ、合計件数
- NFR:95P≤300ms、同時500
BFF(Fastify, Node 20, TS)
import Fastify from "fastify";
const app = Fastify();
app.get("/api/patients", async (req, reply) => {
const q = req.query as { dept?: string; status?: string; page?: string; s?: string; };
const page = Number(q.page ?? 1);
const timeout = 1500; // ms (NFRから逆算)
// 並列呼び出しとタイムアウト制御(擬似)
const [list, total] = await Promise.all([
fetchWithTimeout(`http://svc/list?...`, timeout),
fetchWithTimeout(`http://svc/count?...`, timeout),
]);
return reply.send({ items: list.items, total, pageSize: 20, page });
});
app.listen({ port: 3000 });
React(TSX)
import { useEffect, useState } from "react";
export function PatientList() {
const [state, setState] = useState({ items: [], total: 0, page: 1, loading: false });
useEffect(() => {
setState(s => ({ ...s, loading: true }));
fetch(`/api/patients?page=${state.page}`)
.then(r => r.json())
.then(d => setState(s => ({ ...s, ...d, loading: false })));
}, [state.page]);
return (
<div>
<h2>患者一覧</h2>
{state.loading ? "loading..." : (
<>
<ul>{state.items.map((x: any) => <li key={x.id}>{x.name}</li>)}</ul>
<p>{state.total}件</p>
</>
)}
</div>
);
}
Vue 3(<script setup lang="ts">
)
<script setup lang="ts">
import { ref, watchEffect } from "vue";
const page = ref(1);
const items = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);
watchEffect(async () => {
loading.value = true;
const r = await fetch(`/api/patients?page=${page.value}`);
const d = await r.json();
items.value = d.items; total.value = d.total; loading.value = false;
});
</script>
<template>
<div>
<h2>患者一覧</h2>
<div v-if="loading">loading...</div>
<ul v-else>
<li v-for="x in items" :key="x.id">{{ x.name }}</li>
</ul>
<p>{{ total }}件</p>
</div>
</template>
純JavaScript(理解用ミニ)
<script>
let page = 1;
async function load() {
const d = await fetch(`/api/patients?page=${page}`).then(r=>r.json());
document.querySelector("#total").textContent = d.total + "件";
document.querySelector("#list").innerHTML = d.items.map(x=>`<li>${x.name}</li>`).join("");
}
load();
</script>
<ul id="list"></ul><p id="total"></p>
6. アンチパターン → 改善
- アンチ:Controllerで外部APIを直列多段呼び出し(RTT増, タイムアウト不統一)
- 改善:BFFで並列合成 + 共通のリトライ/タイムアウト戦略 + キャッシュ鍵設計
7. 設計レビュー・チェックリスト
- ユースケースごとにNFRが定量化されているか
- そのNFRからタイムアウト/リトライ/並列のパラメータに落ちているか
- 認可は画面条件に依存するルールをBFFで最終判定しているか
- ADRに代替案と負の帰結まで記録したか
- 観測(トレーシングID、重要メトリクス)は最初から埋め込み済みか
8. まとめ
ユースケース→NFR→ADR→参照構成→薄実装。この一本道で「上流もできる」を可視化できる。コードは薄く、意思決定を厚く残すのが肝要である。