Vue — モック先行 → 本番繋ぎ替え
テクノロジ系 / L11 api — 非同期 UX と API 連携設計
Vue — モック先行 → 本番繋ぎ替え
api / ch03
このチャプターの到達ゴール
| 翌日 |
|---|
| json-server スタブで画面を先に完成させ、バックエンド待ちで止まらない |
| 完成した画面を 1 画面ずつ本番 API パスへ繋ぎ替えられる |
繋ぎ替え漏れ (生 axios・直書き URL の取り残し) を grep で全検出できる |
| 「直書き URL 0 件・共有 api を全 view が import」 を vitest / lint で機械検証できる |
> 題材は 天気ダッシュボード。 現在天気・週間予報・観測地点の 3 画面を持つ。
段階的開発とは (= 画面を先に動かす)
バックエンドが完成するまで画面を作れない、では遅い。
ダミーの API (スタブ) で画面を先に完成させ、後で本物に繋ぎ替える。
| フェーズ | 叩く先 | 進め方 |
|---|---|---|
| ① モック先行 | json-server のスタブ | 画面の 3 状態・レイアウトを完成させる |
| ② 繋ぎ替え | 本番 API | 1 画面ずつ mock パスを外していく |
| ③ 回収 | — | 取り残し (生 axios・直書き URL) を grep で全検出 |
> 一気に全部本番化すると、どこで壊れたか分からなくなる。 1 画面ずつが鉄則。
json-server でスタブ API を立てる
db.json に返したい JSON を置くだけで REST API になる。
// mock/db.json
{
"current": { "tempC": 21, "summary": "晴れ" },
"forecast": [
{ "date": "2026-06-24", "tempC": 23, "summary": "曇り" },
{ "date": "2026-06-25", "tempC": 19, "summary": "雨" }
],
"stations": [
{ "id": 1, "name": "中央観測所" },
{ "id": 2, "name": "臨海観測所" }
]
}npx json-server --watch mock/db.json --port 8000 # → GET /current /forecast /stations が即使える
> バックエンド担当を待たずに、フロントが 今すぐ画面を完成できる。
モック先行で画面を完成させる
ch02 の単一 api インスタンスを そのまま使う。 baseURL を mock に向けるだけ。
# .env.development (モック先行フェーズ) VITE_API_BASE=http://localhost:8000 # ← json-server を指す
// CurrentWeatherView.vue
import { api, toMessage } from '@/lib/apiClient'
async function load() {
loading.value = true; error.value = ''
try {
const res = await api.get('/current') // ← 本番と同じ呼び方。 中身が mock なだけ
current.value = res.data
} catch (e) { error.value = toMessage(e) }
finally { loading.value = false }
}> ポイント: コードは本番と同じ。 .env の向き先が mock か本番かの違いだけ。
> ch02 で一元化しておいたから、繋ぎ替えが env 1 行で済む。
繋ぎ替えは「1 画面ずつ」
本番 API ができたら、env を本番に向ける。 ……が、現実は 不揃いに残骸が残る。
// ❌ CurrentWeatherView だけ、急いでいた頃の生 axios が取り残された
import axios from 'axios' // ← 生 axios の import が残骸
const res = await axios.get('http://localhost:8000/current') // ← 直書き URL も残骸// ✅ ForecastView / StationsView は共有 api に移行済み
import { api } from '@/lib/apiClient'
const res = await api.get('/forecast')> 「3 画面のうち 2 画面は移行済み、1 画面だけ古いまま」 という 不揃いが事故の温床。
> 目視では見落とす。 だから 機械で全検出する。
残骸を grep で全検出する
繋ぎ替え漏れの「臭い」 は 2 種類。 両方を grep で炙り出す。
# 臭い 1: 共有 api を使わず生 axios を直 import している view grep -rn "import axios" src/views/ # → ヒットした view が繋ぎ替え漏れ (期待: 0 件) # 臭い 2: baseURL を使わず URL を直書きしている箇所 grep -rnE "https?://" src/ # → apiClient.ts の env 参照以外でヒットしたら片手落ち (期待: 0 件)
| 期待状態 | 検証 |
|---|---|
| 生 axios import が view に無い | grep "import axios" src/views/ = 0 件 |
| 直書き URL が無い | grep -E "https?://" src/ = 0 件 |
| 全 view が共有 api を import | grep "from '@/lib/apiClient'" src/views/ = view 数と一致 |
> grep は目視より速くて漏れない。 「0 件であること」 を完成判定に使う。
vitest で機械検証する (= 完成を assertion 化)
grep を手で打つ代わりに、テストで自動チェックする。 ソース文字列を走査するだけ。
// topics/api/ch03-mock-to-prod-migration/migration.test.ts
import { describe, it, expect } from 'vitest'
import { readFileSync, readdirSync } from 'node:fs'
import { join } from 'node:path'
const viewsDir = join(__dirname, 'src/views')
const views = readdirSync(viewsDir).filter((f) => f.endsWith('.vue'))
const read = (f: string) => readFileSync(join(viewsDir, f), 'utf-8')
describe('mock → 本番 繋ぎ替えの回収', () => {
it('view に生 axios の直 import が残っていない', () => {
for (const f of views) {
expect(read(f), `${f} に import axios が残存`).not.toMatch(/import\s+axios/)
}
})
it('直書き URL (http(s)://) が view に残っていない', () => {
for (const f of views) {
expect(read(f), `${f} に直書き URL が残存`).not.toMatch(/https?:\/\//)
}
})
it('全 view が共有 api を import している', () => {
for (const f of views) {
expect(read(f), `${f} が @/lib/apiClient を import していない`)
.toMatch(/from '@\/lib\/apiClient'/)
}
})
})> 「直書き URL 0 件」 「共有 api が全 view に存在」 を テストが守る。 CI に乗せれば再発も防げる。
なぜ env で切り替えると安全か
# モック先行 VITE_API_BASE=http://localhost:8000 # 本番 VITE_API_BASE=https://api.example.test
| 切り替え方 | 結果 |
|---|---|
| ❌ 各 view の URL を手で書き換え | 1 画面だけ直し忘れ → 不揃い → 本番で mock を叩き続ける事故 |
✅ .env を 1 行変える | 全画面が一斉に切り替わる。 view のソースは 1 行も触らない |
> ch02 で api インスタンスに baseURL を集約したから、この切り替えが効く。
> 一元化は「繋ぎ替えのとき」 に最大の恩恵を返す。
わざと残骸を作って回収する
| 手順 | 操作 | 観察 |
|---|---|---|
| ① 残骸を作る | 1 画面だけ import axios + 直書き URL に戻す | vitest がその view を名指しで FAIL |
| ② grep で特定 | grep -rn "import axios" src/views/ | 残骸の view が 1 件ヒット |
| ③ 回収 | 共有 api + 相対パスに直す | vitest が green |
| ④ 再発防止 | テストを CI に入れる | 次に誰かが直書きしたら CI が落ちる |
> ⚠️ 「動いているから OK」 ではなく 「残骸が 0 件であること」 を完成条件にする。
チャプター完了サイン
- [ ]
json-serverのスタブで 3 画面が 3 状態込みで完成している - [ ] 本番 API への切り替えが
.env1 行で済む (view のソースは触らない) - [ ]
grep -rn "import axios" src/views/が 0 件 - [ ]
grep -rnE "https?://" src/が 0 件 (apiClient の env 参照を除く) - [ ] 全 view が
@/lib/apiClientを import している (= 数が view 数と一致) - [ ] vitest の 繋ぎ替え回収テストが green
- [ ] 「1 画面ずつ繋ぎ替える理由」 を 1 文で言える
次に学ぶ領域
| 領域 | 名前 |
|---|---|
| 高機能なモック | MSW (Service Worker でネットワーク層をモック) |
| 型の自動生成 | OpenAPI スキーマからクライアント生成 |
| キャッシュ / 再取得 | TanStack Query |
| 契約テスト | フロント↔バックの contract testing |
> json-server は「最速で画面を動かす」 ための入口。 規模が大きくなったら MSW / OpenAPI に進む地図を持っておく。
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
ch03 — 演習
題材: 天気ダッシュボード。 現在天気 (CurrentWeatherView) / 週間予報 (ForecastView) / 観測地点 (StationsView) の 3 画面。
Lv1: json-server スタブで画面を先行で完成させる
バックエンド無しで 3 画面を 3 状態込みで作る。
// mock/db.json
{
"current": { "tempC": 21, "summary": "晴れ" },
"forecast": [{ "date": "2026-06-24", "tempC": 23, "summary": "曇り" }],
"stations": [{ "id": 1, "name": "中央観測所" }]
}npx json-server --watch mock/db.json --port 8000
# .env.development VITE_API_BASE=http://localhost:8000 # ← まずは mock を指す
// CurrentWeatherView.vue (ForecastView / StationsView も同形)
import { api, toMessage } from '@/lib/apiClient'
// TODO 1: api.get('/current') で取得し loading/error/success の 3 状態を出す検証
npx json-server起動中、3 画面が 読み込み中 → データ表示 に切り替わる- json-server を止めると エラー + 再読み込み が出る (3 状態が効いている)
Lv2: 1 画面ずつ本番 API へ繋ぎ替え、残骸を grep + vitest で回収
配布される starter は 不揃い: ForecastView / StationsView は共有 api 済みだが、CurrentWeatherView だけ生 axios + 直書き URL が取り残されている。
// CurrentWeatherView.vue ← 残骸あり (これを回収する)
import axios from 'axios' // ← 撤去対象
const res = await axios.get('http://localhost:8000/current') // ← 撤去対象// TODO 2: import axios → import { api } from '@/lib/apiClient'
// TODO 3: axios.get('http://localhost:8000/current') → api.get('/current')繋ぎ替え後、本番へ切り替えは env 1 行:
# .env.production VITE_API_BASE=https://api.example.test
検証 (grep)
grep -rn "import axios" src/views/ # 期待: 0 件 grep -rnE "https?://" src/ # 期待: 0 件 (apiClient の env 参照を除く) grep -rn "from '@/lib/apiClient'" src/views/ # 期待: view 数と一致
検証 (vitest)
cd frontend/vue pnpm test topics/api/ch03-mock-to-prod-migration
- 「view に生 axios が残っていない」 = green
- 「直書き URL が残っていない」 = green
- 「全 view が共有 api を import」 = green
提出物
「3 画面を一気にではなく 1 画面ずつ繋ぎ替える理由」 を 1 文で書け。
チャレンジ
- わざと 1 画面を生 axios + 直書き URL に戻し、vitest が その view を名指しで FAIL させるのを確認 → 回収して green に戻す
migration.test.tsを CI (GitHub Actions など) のステップに入れ、直書き URL が混入した PR が落ちることを確認する- mock の
db.jsonに遅延 (--delay) を足し、ローディング表示が長く見えるのを観察する
ヒント
- 残骸検出の grep は
src/views/に絞る と apiClient.ts 自体の env 参照を巻き込まない - 直書き URL の正規表現は
https?://。 コメント内の URL もヒットするので、教材中は URL を書かない /example.testに統一する - env を変えたら dev サーバを再起動 しないと
import.meta.envが反映されないことがある - 「動いているか」 でなく 「残骸が 0 件か」 を完成条件にする。 動いていても mock を叩き続けている事故があり得る