採点されません。匿名です。 間違えることは学習の一部です。読むだけでもかまいません。

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 状態・レイアウトを完成させる
② 繋ぎ替え本番 API1 画面ずつ 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 を importgrep "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 への切り替えが .env 1 行で済む (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 を叩き続けている事故があり得る