L08 ch04 — fetch で外部 API
テクノロジ系 / L08 vue-basic — Vue 3 入門
L08 ch04 — fetch で外部 API
ブラウザ標準の fetch API で連携
ゴール
- fetch で GET / POST
- async / await
- onMounted で起動時取得
GET (最小)
<script setup>
import { ref, onMounted } from 'vue'
const items = ref([])
async function loadItems() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos')
items.value = await res.json()
}
onMounted(loadItems)
</script>
<template>
<p>件数: {{ items.length }}</p>
<ul>
<li v-for="item in items.slice(0, 5)" :key="item.id">
{{ item.title }}
</li>
</ul>
</template>エラー処理
async function loadItems() {
try {
const res = await fetch('http://localhost:8000/tasks')
if (!res.ok) { // ← 必須 (fetch は 404 でも throw しない)
throw new Error(`HTTP ${res.status}`)
}
items.value = await res.json()
} catch (e) {
console.error('取得失敗:', e)
items.value = []
}
}POST
async function addTask(title) {
await fetch('http://localhost:8000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
})
await loadTasks() // 再取得で更新
}→ headers の Content-Type を忘れると 422
onMounted
コンポーネントが画面に表示された 直後 に実行。
データ取得を 1 度だけしたいときに使う。
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('マウント完了')
})
</script>罠
- fetch は 404 でも例外を投げない →
if (!res.ok)判定必須 - CORS エラー → サーバー側で
CORSMiddleware設定 (L08 ch06 で扱う) - ネットワーク遅延 → ローディング表示
URL にクエリパラメータを埋める
検索のように動的な値を URL に乗せたいときは テンプレートリテラル が定石。
const keyword = ref('')
async function search() {
const url = `http://localhost:8000/search?q=${encodeURIComponent(keyword.value)}`
const res = await fetch(url)
results.value = await res.json()
}- バッククォート `
`で囲み、${...}` で値を埋め込む - 空白・日本語・記号は
encodeURIComponent()でエスケープする (+や&で URL が壊れる事故を防ぐ) - 複数キーなら
?q=...&page=2のように&で繋ぐ感覚
ボタンクリックで fetch を発火
onMounted は「画面表示と同時に 1 回」だが、検索・送信は ユーザー操作起点 で動かしたい。
このときは @click にハンドラを直接渡すパターンを使う。
<script setup>
const keyword = ref('')
const results = ref([])
async function search() {
const url = `http://localhost:8000/search?q=${encodeURIComponent(keyword.value)}`
const res = await fetch(url)
results.value = await res.json()
}
</script>
<template>
<input v-model="keyword" placeholder="検索ワード" />
<button @click="search">検索</button>
</template>onMounted= 起動時 1 回@click= ユーザー操作の度に発火- インラインに書くなら
@click="async () => { await search() }"の形でも OK
結果の有無で表示を分ける
検索後は「件数 0」と「件数 > 0」で見せ方を変えるのが UX の基本。
v-if / v-else で分岐できる。
<template>
<ul v-if="results.length > 0">
<li v-for="r in results" :key="r.id">{{ r.title }}</li>
</ul>
<p v-else>該当する結果はありません</p>
</template>v-ifで「真のときに描画」v-elseは 直前の v-if と同じ階層 に書く (間に別要素を挟まない)- 3 分岐以上なら
v-else-ifで中間条件を足す感覚
drill
drill/ — fill-fetch
ハンズオン
ch04-fetch 演習ガイド
受講生向け演習問題集
採点の仕組み (読んでから始めてください)
採点テストは 「お題の通りに動くか」 を機械的にチェックします。
ドリルごとに 3つのタイプ があるので、各ドリル冒頭の 【タイプ】 を確認してください。
タイプ fill (穴埋め問題) — 文法・APIを知っているか
___(3個並んだアンダースコア) の空欄を埋める- 既存の
<template>のテキストやボタン順は そのまま - ロジック自由度: 低 (お題どおりに直す)
タイプ add (機能追加問題) — 既存コードに機能を足せるか
- 既存コードを残したまま、新しい機能を 追加 する
- 実装方法は自由 (関数名・変数名・内部ロジックは好きに)
- ただしテストが要求する見た目 (表示テキスト・ボタンの並び順) は守る
- 既存テストを壊さないこと
タイプ build (ゼロベース実装問題) — 仕様から実装を組み立てられるか
- ほぼ空の
App.vueから 自力で全部書く <template>構造・id/class・内部ロジック: 完全に自由- 守るのは画面の機能仕様だけ: 表示するテキスト・ボタンの操作で何が起きるか
- 一番ハードだが、実装力が一番つくタイプ
3タイプ共通で守ること
- ファイル名 (
App.vue) は変えない tests/フォルダは触らない (採点ロジック)- テンプレートの 表示テキスト (「カウント:」等) はテストが見ているので残す
このガイドのドリル一覧
| # | ドリル名 | タイトル | タイプ | 難易度 |
|---|---|---|---|---|
| 1 | 01-fill-fetch | fetch + onMounted + async/await - API データ取得 | 🔧 穴埋め | ★ |
| 2 | 02-build-search-app | fetch + クエリパラメータ + v-if/v-else - 検索アプリを白紙から | 🛠️ ゼロ実装 | ★★ |
> 詰まったら ヒント ページを開く。それでも分からなければ講師に質問。
ドリル 1: fetch + onMounted + async/await - API データ取得
ドリル名: 01-fill-fetch
> 🔧 【タイプ: 穴埋め問題】 ___ の部分を埋めて完成させてください。既存のコード構造は変えないこと。
問題
___ を埋めて、画面表示時に JSONPlaceholder の todos を取得して一覧表示するアプリを完成させよ。___ は全部で5箇所ある。
<script setup>
import { ref, ___ } from 'vue' <!-- ___1: 画面表示時に1回実行するライフサイクル関数 -->
const items = ref([])
___ function loadItems() { <!-- ___2: 非同期関数を宣言するキーワード -->
const res = ___ fetch('https://jsonplaceholder.typicode.com/todos') <!-- ___3: 非同期処理の結果を待つキーワード -->
items.value = ___ res.json() <!-- ___4: 同じく結果を待つ -->
}
___(loadItems) <!-- ___5: ___1 で import した関数を呼ぶ -->
</script>
<template>
<p>件数: {{ items.length }}</p>
<ul>
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
</ul>
</template>確認方法
App.vueに貼り付けてnpm run devで起動- ページを開くと自動で 200 件のリストが表示される
- F12 → Network タブで
todosリクエストが見える
ドリル 1: ヒント
ドリル名: 01-fill-fetch
> ⚠️ 自力で考えてから読んでください
___1/___5: 画面表示時に 1 回実行する Vue のライフサイクル関数(onから始まる)___2: 非同期関数の宣言キーワード(4 文字)___3/___4: Promise の解決を待つキーワード(5 文字)
ドリル 2: fetch + クエリパラメータ + v-if/v-else - 検索アプリを白紙から
ドリル名: 02-build-search-app
> 🛠️ 【タイプ: ゼロベース実装問題】 ほぼ空の App.vue から仕様を読んで自力で実装してください。<template> 構造・id/class・内部ロジックは完全に自由。ただしテストが見る 画面の表示テキスト・ボタンの動作 は仕様どおりに揃えること。
問題
Vue で「キーワード検索 → 結果表示」のミニアプリを App.vue に 白紙から 実装してください。
仕様
API はモック (テストで fetch を stub):
GET http://localhost:8000/search?q=<keyword>
→ [{ "id": 1, "title": "..." }, ...]画面要件
- 検索キーワード入力欄 (テキスト)
- 「検索」ボタン クリックで fetch → 結果一覧を表示
- 結果が空なら 「該当なし」 と表示
- ローディング中は 「検索中...」 と表示 (任意、テストはここを必須にしない)
守る (テストが見ている)
- 「検索」ボタンが画面にある
- 「検索」クリックで
fetchが呼ばれる (URL に/searchを含む) - 入力したキーワードが URL の
q=パラメータに反映される - 取得した結果のタイトルが画面に表示される
- 結果が空のとき「該当なし」と表示
このドリルで学ぶこと
| 学習要素 | 学習資料との対応 |
|---|---|
| テンプレートリテラルでクエリパラメータ付き URL を組み立てる | ch04 slide「URL にクエリパラメータを埋める」 |
encodeURIComponent で値を安全にエスケープ | ch04 slide「URL にクエリパラメータを埋める」 |
| ボタンクリックで fetch を発火 (onMounted との対比) | ch04 slide「ボタンクリックで fetch を発火」 |
v-if / v-else で結果の有無を分岐表示 | ch04 slide「結果の有無で表示を分ける」 |
このドリルは 「非同期処理 + 分岐UI」の最小形。実務でほぼ必ず出る検索画面パターンを体得します。
ドリル 2: ヒント
ドリル名: 02-build-search-app
> ⚠️ 自力で考えてから読んでください
段階1: 入力欄と結果配列
import { ref } from 'vue'
const keyword = ref('')
const results = ref([])段階2: 検索ボタンと fetch
async function search() {
const url = `http://localhost:8000/search?q=${keyword.value}`
const res = await fetch(url)
results.value = await res.json()
}段階3: 結果表示と「該当なし」
<ul v-if="results.length > 0">
<li v-for="item in results" :key="item.id">{{ item.title }}</li>
</ul>
<p v-else>該当なし</p>ただし「最初から該当なしと出る」のは違和感あるので searched フラグで「一度でも検索したか」を持つと自然。
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
ch04-fetch 演習ガイド
受講生向け演習問題集
採点の仕組み (読んでから始めてください)
採点テストは 「お題の通りに動くか」 を機械的にチェックします。
ドリルごとに 3つのタイプ があるので、各ドリル冒頭の 【タイプ】 を確認してください。
タイプ fill (穴埋め問題) — 文法・APIを知っているか
___(3個並んだアンダースコア) の空欄を埋める- 既存の
<template>のテキストやボタン順は そのまま - ロジック自由度: 低 (お題どおりに直す)
タイプ add (機能追加問題) — 既存コードに機能を足せるか
- 既存コードを残したまま、新しい機能を 追加 する
- 実装方法は自由 (関数名・変数名・内部ロジックは好きに)
- ただしテストが要求する見た目 (表示テキスト・ボタンの並び順) は守る
- 既存テストを壊さないこと
タイプ build (ゼロベース実装問題) — 仕様から実装を組み立てられるか
- ほぼ空の
App.vueから 自力で全部書く <template>構造・id/class・内部ロジック: 完全に自由- 守るのは画面の機能仕様だけ: 表示するテキスト・ボタンの操作で何が起きるか
- 一番ハードだが、実装力が一番つくタイプ
3タイプ共通で守ること
- ファイル名 (
App.vue) は変えない tests/フォルダは触らない (採点ロジック)- テンプレートの 表示テキスト (「カウント:」等) はテストが見ているので残す
このガイドのドリル一覧
| # | ドリル名 | タイトル | タイプ | 難易度 |
|---|---|---|---|---|
| 1 | 01-fill-fetch | fetch + onMounted + async/await - API データ取得 | 🔧 穴埋め | ★ |
| 2 | 02-build-search-app | fetch + クエリパラメータ + v-if/v-else - 検索アプリを白紙から | 🛠️ ゼロ実装 | ★★ |
> 詰まったら ヒント ページを開く。それでも分からなければ講師に質問。
ドリル 1: fetch + onMounted + async/await - API データ取得
ドリル名: 01-fill-fetch
> 🔧 【タイプ: 穴埋め問題】 ___ の部分を埋めて完成させてください。既存のコード構造は変えないこと。
問題
___ を埋めて、画面表示時に JSONPlaceholder の todos を取得して一覧表示するアプリを完成させよ。___ は全部で5箇所ある。
<script setup>
import { ref, ___ } from 'vue' <!-- ___1: 画面表示時に1回実行するライフサイクル関数 -->
const items = ref([])
___ function loadItems() { <!-- ___2: 非同期関数を宣言するキーワード -->
const res = ___ fetch('https://jsonplaceholder.typicode.com/todos') <!-- ___3: 非同期処理の結果を待つキーワード -->
items.value = ___ res.json() <!-- ___4: 同じく結果を待つ -->
}
___(loadItems) <!-- ___5: ___1 で import した関数を呼ぶ -->
</script>
<template>
<p>件数: {{ items.length }}</p>
<ul>
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
</ul>
</template>確認方法
App.vueに貼り付けてnpm run devで起動- ページを開くと自動で 200 件のリストが表示される
- F12 → Network タブで
todosリクエストが見える
ドリル 1: ヒント
ドリル名: 01-fill-fetch
> ⚠️ 自力で考えてから読んでください
___1/___5: 画面表示時に 1 回実行する Vue のライフサイクル関数(onから始まる)___2: 非同期関数の宣言キーワード(4 文字)___3/___4: Promise の解決を待つキーワード(5 文字)
ドリル 2: fetch + クエリパラメータ + v-if/v-else - 検索アプリを白紙から
ドリル名: 02-build-search-app
> 🛠️ 【タイプ: ゼロベース実装問題】 ほぼ空の App.vue から仕様を読んで自力で実装してください。<template> 構造・id/class・内部ロジックは完全に自由。ただしテストが見る 画面の表示テキスト・ボタンの動作 は仕様どおりに揃えること。
問題
Vue で「キーワード検索 → 結果表示」のミニアプリを App.vue に 白紙から 実装してください。
仕様
API はモック (テストで fetch を stub):
GET http://localhost:8000/search?q=<keyword>
→ [{ "id": 1, "title": "..." }, ...]画面要件
- 検索キーワード入力欄 (テキスト)
- 「検索」ボタン クリックで fetch → 結果一覧を表示
- 結果が空なら 「該当なし」 と表示
- ローディング中は 「検索中...」 と表示 (任意、テストはここを必須にしない)
守る (テストが見ている)
- 「検索」ボタンが画面にある
- 「検索」クリックで
fetchが呼ばれる (URL に/searchを含む) - 入力したキーワードが URL の
q=パラメータに反映される - 取得した結果のタイトルが画面に表示される
- 結果が空のとき「該当なし」と表示
このドリルで学ぶこと
| 学習要素 | 学習資料との対応 |
|---|---|
| テンプレートリテラルでクエリパラメータ付き URL を組み立てる | ch04 slide「URL にクエリパラメータを埋める」 |
encodeURIComponent で値を安全にエスケープ | ch04 slide「URL にクエリパラメータを埋める」 |
| ボタンクリックで fetch を発火 (onMounted との対比) | ch04 slide「ボタンクリックで fetch を発火」 |
v-if / v-else で結果の有無を分岐表示 | ch04 slide「結果の有無で表示を分ける」 |
このドリルは 「非同期処理 + 分岐UI」の最小形。実務でほぼ必ず出る検索画面パターンを体得します。
ドリル 2: ヒント
ドリル名: 02-build-search-app
> ⚠️ 自力で考えてから読んでください
段階1: 入力欄と結果配列
import { ref } from 'vue'
const keyword = ref('')
const results = ref([])段階2: 検索ボタンと fetch
async function search() {
const url = `http://localhost:8000/search?q=${keyword.value}`
const res = await fetch(url)
results.value = await res.json()
}段階3: 結果表示と「該当なし」
<ul v-if="results.length > 0">
<li v-for="item in results" :key="item.id">{{ item.title }}</li>
</ul>
<p v-else>該当なし</p>ただし「最初から該当なしと出る」のは違和感あるので searched フラグで「一度でも検索したか」を持つと自然。
ch04-fetch 演習解説
⚠️ 自力でドリルを解いてから読んでください
ドリル 1: fetch + onMounted + async/await - API データ取得 — 模範解答
ドリル名: 01-fill-fetch
<script setup>
import { ref, onMounted } from 'vue'
const items = ref([])
async function loadItems() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos')
items.value = await res.json()
}
onMounted(loadItems)
</script>
<template>
<p>件数: {{ items.length }}</p>
<ul>
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
</ul>
</template>ドリル 1: fetch + onMounted + async/await - API データ取得 — 解説
ドリル名: 01-fill-fetch
| 空欄 | 解答 |
|---|---|
___1 | onMounted |
___2 | async |
___3 | await |
___4 | await |
___5 | onMounted |
観察ポイント
asyncを付けずにawaitを書く受講生が多い → エラーメッセージで気づけるonMounted(() => loadItems())のように関数を渡してもよい(同じ結果)onMounted(loadItems())と()を付けるとその場で実行されて壊れる → 関数を「渡す」概念の理解
ドリル 2: fetch + クエリパラメータ + v-if/v-else - 検索アプリを白紙から — 模範解答
ドリル名: 02-build-search-app
<script setup>
import { ref } from 'vue'
const keyword = ref('')
const results = ref([])
const searched = ref(false)
async function search() {
const url = `http://localhost:8000/search?q=${encodeURIComponent(keyword.value)}`
const res = await fetch(url)
results.value = await res.json()
searched.value = true
}
</script>
<template>
<div>
<input v-model="keyword" type="text" placeholder="キーワード" />
<button @click="search">検索</button>
<ul v-if="results.length > 0">
<li v-for="item in results" :key="item.id">{{ item.title }}</li>
</ul>
<p v-else-if="searched">該当なし</p>
</div>
</template>ドリル 2: fetch + クエリパラメータ + v-if/v-else - 検索アプリを白紙から — 解説
ドリル名: 02-build-search-app
検索 UI のパターン
- 入力 → クリック → fetch → 表示 の流れ
- URLエンコード :
encodeURIComponent(keyword.value)で日本語や記号を安全に - 空結果のハンドリング :
v-if="results.length > 0"で分岐、v-elseで「該当なし」 - 初回表示 : 検索前は何も表示しないのが自然 →
searchedフラグ
実装が複数あり得る点
- 検索のトリガー: ボタンクリック / Enter キー押下 /
@inputでインクリメンタル検索 - ローディング表示: あり / なし
- 検索結果の型:
string[]/{id, title}[]/{id, title, description}[] - エラー処理: try/catch / fetch の
res.okチェック
Q: fetch + onMounted の空欄を埋める
問題
___ を埋めて、画面表示時に JSONPlaceholder の todos を取得して一覧表示するアプリを完成させよ。___ は全部で5箇所ある。
<script setup>
import { ref, ___ } from 'vue' <!-- ___1: 画面表示時に1回実行するライフサイクル関数 -->
const items = ref([])
___ function loadItems() { <!-- ___2: 非同期関数を宣言するキーワード -->
const res = ___ fetch('https://jsonplaceholder.typicode.com/todos') <!-- ___3: 非同期処理の結果を待つキーワード -->
items.value = ___ res.json() <!-- ___4: 同じく結果を待つ -->
}
___(loadItems) <!-- ___5: ___1 で import した関数を呼ぶ -->
</script>
<template>
<p>件数: {{ items.length }}</p>
<ul>
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
</ul>
</template>確認方法
App.vueに貼り付けてnpm run devで起動- ページを開くと自動で 200 件のリストが表示される
- F12 → Network タブで
todosリクエストが見える
ヒント
___1/___5: 画面表示時に 1 回実行する Vue のライフサイクル関数(onから始まる)___2: 非同期関数の宣言キーワード(4 文字)___3/___4: Promise の解決を待つキーワード(5 文字)
Q: 検索アプリを ゼロから実装する
問題
Vue で「キーワード検索 → 結果表示」のミニアプリを App.vue に 白紙から 実装してください。
仕様
API はモック (テストで fetch を stub):
GET http://localhost:8000/search?q=<keyword>
→ [{ "id": 1, "title": "..." }, ...]画面要件
- 検索キーワード入力欄 (テキスト)
- 「検索」ボタン クリックで fetch → 結果一覧を表示
- 結果が空なら 「該当なし」 と表示
- ローディング中は 「検索中...」 と表示 (任意、テストはここを必須にしない)
守る (テストが見ている)
- 「検索」ボタンが画面にある
- 「検索」クリックで
fetchが呼ばれる (URL に/searchを含む) - 入力したキーワードが URL の
q=パラメータに反映される - 取得した結果のタイトルが画面に表示される
- 結果が空のとき「該当なし」と表示
このドリルで学ぶこと
| 学習要素 | 学習資料との対応 |
|---|---|
| テンプレートリテラルでクエリパラメータ付き URL を組み立てる | ch04 slide「URL にクエリパラメータを埋める」 |
encodeURIComponent で値を安全にエスケープ | ch04 slide「URL にクエリパラメータを埋める」 |
| ボタンクリックで fetch を発火 (onMounted との対比) | ch04 slide「ボタンクリックで fetch を発火」 |
v-if / v-else で結果の有無を分岐表示 | ch04 slide「結果の有無で表示を分ける」 |
このドリルは 「非同期処理 + 分岐UI」の最小形。実務でほぼ必ず出る検索画面パターンを体得します。
ヒント
段階1: 入力欄と結果配列
import { ref } from 'vue'
const keyword = ref('')
const results = ref([])段階2: 検索ボタンと fetch
async function search() {
const url = `http://localhost:8000/search?q=${keyword.value}`
const res = await fetch(url)
results.value = await res.json()
}段階3: 結果表示と「該当なし」
<ul v-if="results.length > 0">
<li v-for="item in results" :key="item.id">{{ item.title }}</li>
</ul>
<p v-else>該当なし</p>ただし「最初から該当なしと出る」のは違和感あるので searched フラグで「一度でも検索したか」を持つと自然。
解説
検索 UI のパターン
- 入力 → クリック → fetch → 表示 の流れ
- URLエンコード :
encodeURIComponent(keyword.value)で日本語や記号を安全に - 空結果のハンドリング :
v-if="results.length > 0"で分岐、v-elseで「該当なし」 - 初回表示 : 検索前は何も表示しないのが自然 →
searchedフラグ
実装が複数あり得る点
- 検索のトリガー: ボタンクリック / Enter キー押下 /
@inputでインクリメンタル検索 - ローディング表示: あり / なし
- 検索結果の型:
string[]/{id, title}[]/{id, title, description}[] - エラー処理: try/catch / fetch の
res.okチェック