L08 ch06 — Vue + FastAPI フルスタック連携
テクノロジ系 / L08 vue-basic — Vue 3 入門
L08 ch06 — Vue + FastAPI フルスタック連携
フロント (5173) ↔ バックエンド (8000)
ゴール
- Vue (5173) → FastAPI (8000) で CRUD
- CORS 問題の解決
- 実用的フルスタックアプリの土台
構成図
ブラウザ
↓ http://localhost:5173 で Vue 表示
Vue (Vite)
↓ fetch('http://localhost:8000/tasks')
FastAPI (uvicorn)
↓ SQLAlchemy
SQLite罠: CORS エラー
fetch('http://localhost:8000/tasks')
// → Console:
// Access to fetch at 'http://localhost:8000/tasks' from
// origin 'http://localhost:5173' has been blocked by CORS policy→ FastAPI 側で CORSMiddleware を設定 (L14 ch01 を先に)
CRUD 統合実装
<script setup>
import { ref, onMounted } from 'vue'
const tasks = ref([])
const newTitle = ref('')
const API = 'http://localhost:8000'
async function loadTasks() {
const res = await fetch(`${API}/tasks`)
tasks.value = await res.json()
}
async function addTask() {
await fetch(`${API}/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle.value }),
})
newTitle.value = ''
await loadTasks()
}
onMounted(loadTasks)
</script>PATCH / DELETE
async function toggleDone(task) {
await fetch(`${API}/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ done: !task.done }),
})
await loadTasks()
}
async function deleteTask(id) {
await fetch(`${API}/tasks/${id}`, { method: 'DELETE' })
await loadTasks()
}環境変数化
ハードコード http://localhost:8000 は本番で問題:
// .env.development VITE_API_BASE_URL=http://localhost:8000 // .env.production VITE_API_BASE_URL=https://api.my-app.com // 使用 const API = import.meta.env.VITE_API_BASE_URL
チェックボックスと状態の同期
v-model を使うとローカル変更だけで完結してしまう。
サーバーと同期 したいときは :checked + @change で PATCH を発火する。
<template>
<li v-for="t in tasks" :key="t.id">
<input
type="checkbox"
:checked="t.done"
@change="toggleDone(t)"
/>
{{ t.title }}
</li>
</template>
<script setup>
async function toggleDone(task) {
await fetch(`${API}/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ done: !task.done }),
})
await loadTasks() // サーバー側の真の状態を再取得
}
</script>:checked= 表示反映 (一方向)@change= ユーザー操作を捕まえる- PATCH 後に
loadTasks()で再描画する感覚
動的スタイルで状態を表す
完了タスクに 取り消し線 を引くと UI として一気にプロっぽくなる。
:style にオブジェクトを渡せば条件で切り替えられる。
<template>
<li v-for="t in tasks" :key="t.id">
<input type="checkbox" :checked="t.done" @change="toggleDone(t)" />
<span :style="{ textDecoration: t.done ? 'line-through' : 'none' }">
{{ t.title }}
</span>
</li>
</template>:style="{ key: 値 }"の形で インラインスタイル をバインド- CSS プロパティは
text-decoration→textDecorationのように キャメルケース で書く - 複雑になったら
:class="{ done: t.done }"+ CSS クラス側で定義する手もある
drill
drill/ — fill-patch-delete, add-filter, add-priority
ハンズオン
ch06-fastapi 演習ガイド
受講生向け演習問題集
採点の仕組み (読んでから始めてください)
採点テストは 「お題の通りに動くか」 を機械的にチェックします。
ドリルごとに 3つのタイプ があるので、各ドリル冒頭の 【タイプ】 を確認してください。
タイプ fill (穴埋め問題) — 文法・APIを知っているか
___(3個並んだアンダースコア) の空欄を埋める- 既存の
<template>のテキストやボタン順は そのまま - ロジック自由度: 低 (お題どおりに直す)
タイプ add (機能追加問題) — 既存コードに機能を足せるか
- 既存コードを残したまま、新しい機能を 追加 する
- 実装方法は自由 (関数名・変数名・内部ロジックは好きに)
- ただしテストが要求する見た目 (表示テキスト・ボタンの並び順) は守る
- 既存テストを壊さないこと
タイプ build (ゼロベース実装問題) — 仕様から実装を組み立てられるか
- ほぼ空の
App.vueから 自力で全部書く <template>構造・id/class・内部ロジック: 完全に自由- 守るのは画面の機能仕様だけ: 表示するテキスト・ボタンの操作で何が起きるか
- 一番ハードだが、実装力が一番つくタイプ
3タイプ共通で守ること
- ファイル名 (
App.vue) は変えない tests/フォルダは触らない (採点ロジック)- テンプレートの 表示テキスト (「カウント:」等) はテストが見ているので残す
このガイドのドリル一覧
| # | ドリル名 | タイトル | タイプ | 難易度 |
|---|---|---|---|---|
| 1 | 01-fill-patch-delete | fetch PATCH/DELETE - CRUD 4動詞の連携 | 🔧 穴埋め | ★ |
| 2 | 02-add-filter | computed + filter - リスト絞り込み | ➕ 機能追加 | ★ |
| 3 | 03-add-priority | select + 動的バインド - 優先度フィールド追加 | ➕ 機能追加 | ★ |
| 4 | 04-build-fullstack-todo | onMounted + fetch + チェックボックス + 動的スタイル - フルスタック Todo を白紙から | 🛠️ ゼロ実装 | ★★★ |
> 詰まったら ヒント ページを開く。それでも分からなければ講師に質問。
ドリル 1: fetch PATCH/DELETE - CRUD 4動詞の連携
ドリル名: 01-fill-patch-delete
> 🔧 【タイプ: 穴埋め問題】 ___ の部分を埋めて完成させてください。既存のコード構造は変えないこと。
問題
___ を埋めて、完了切り替え(PATCH)と削除(DELETE)を FastAPI と連携させよ。___ は全部で4箇所ある。
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: '___', <!-- ___1: 更新に使う HTTP メソッド -->
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: task.title, done: !task.done }),
})
await ___() <!-- ___2: 画面を最新状態に更新する関数 -->
}
async function deleteTask(id) {
await fetch(`http://localhost:8000/tasks/${___}`, { <!-- ___3: URL に埋め込む変数名 -->
method: '___', <!-- ___4: 削除に使う HTTP メソッド -->
})
await loadTasks()
}確認方法
POST /tasksでタスクを2件追加する- チェックボックスをクリックすると
doneが反転してtasks.jsonに反映される - 「削除」ボタンをクリックするとリストと
tasks.jsonから消える
ドリル 1: ヒント
ドリル名: 01-fill-patch-delete
> ⚠️ 自力で考えてから読んでください
___1: 既存リソースの部分更新に使う HTTP メソッド(PUT は全体置き換え)___2:onMountedで呼んでいる「一覧を取得して画面に反映する関数」と同じもの___3:deleteTask(id)で受け取った引数をそのまま URL に入れる___4: リソースを削除する HTTP メソッド
ドリル 2: computed + filter - リスト絞り込み
ドリル名: 02-add-filter
> ➕ 【タイプ: 機能追加問題】 既存コードを残したまま新しい機能を追加してください。実装は自由ですが、テストが要求する I/F・表示テキストは守ること。
ベースコード(base/App.vue)は GET/POST/PATCH/DELETE がすべて動く状態。
以下の機能を追加せよ。
追加する機能
- フィルタボタン: 「すべて」「未完了のみ」「完了のみ」の3つのボタンを追加する
- 絞り込み表示: ボタンの状態に応じて表示するタスクを絞り込む
- 件数表示: 「表示中 X 件 / 全 Y 件」を画面下部に表示する
ヒント
computedを使ってtasksから絞り込んだ配列を派生させる- フィルタ状態は
ref('all')のような文字列で管理する('all'/'active'/'done') v-forはtasksの代わりに computed の配列を使う
確認方法
- タスクを追加して一部を完了にする
- 「未完了のみ」で未完了タスクだけ表示される
- 「完了のみ」で完了タスクだけ表示される
- 件数が正しく変わる
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
- 現在選択中のフィルタボタンに
activeクラスを付け、色を変えて「いま何で絞り込み中か」を視覚的に分かりやすくする - タイトル検索用の入力欄を追加し、
computed内でフィルタ条件 + 文字列部分一致の AND で絞り込む - フィルタ状態 (
'all'/'active'/'done') をlocalStorageに保存して、リロード後も最後に選んだフィルタが復元されるようにする
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ドリル 2: ヒント
ドリル名: 02-add-filter
> ⚠️ 自力で考えてから読んでください
> {{HINT}}
ドリル 3: select + 動的バインド - 優先度フィールド追加
ドリル名: 03-add-priority
> ➕ 【タイプ: 機能追加問題】 既存コードを残したまま新しい機能を追加してください。実装は自由ですが、テストが要求する I/F・表示テキストは守ること。
base/ には FastAPI(main.py)と Vue(App.vue)が入っている。
バックエンドとフロントエンドの両方を改造して、タスクに優先度を追加せよ。
追加する仕様
FastAPI 側(main.py)
POST /tasksのボディにpriorityを追加する(型:int、範囲: 1〜3、デフォルト: 2)PATCH /tasks/{task_id}でもpriorityを更新できるようにするpriorityが 1〜3 以外のとき400 Bad Requestを返す
Vue 側(App.vue)
- タスク追加フォームに「優先度」の
<select>を追加する(選択肢: 1・2・3) - 各タスクの表示に priority に応じた色をつける
- 1(高): 赤(color: red)
- 2(中): 黒(デフォルト)
- 3(低): グレー(color: gray)
- PATCH 時も
priorityを送る
確認方法
priority: 1でタスクを追加 → タイトルが赤で表示されるpriority: 3でタスクを追加 → タイトルがグレーで表示されるPOST /tasksに{"title": "test", "priority": 5}を送る → 400 が返る- Swagger UI で tasks.json を確認して
priorityが保存されていること
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
- Vue 側でタスク一覧を
priority昇順で並び替えて表示する (computedでsort()) - 優先度ごとの件数 ("高: 2件 / 中: 5件 / 低: 1件") をフッタに表示する
- 各タスクの背景色や左ボーダー色も priority に応じて変え、色だけでなくレイアウトでも優先度が伝わるようにする
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ドリル 3: ヒント
ドリル名: 03-add-priority
> ⚠️ 自力で考えてから読んでください
FastAPI 側
body.get("priority", 2)でデフォルト値 2 を設定できる- バリデーション:
if priority not in (1, 2, 3): raise HTTPException(400, ...) new_taskの dict に"priority": priorityを追加する- PATCH も同様に
priorityをt.update(...)の中に含める
Vue 側
newPriority = ref(2)で優先度の状態を管理する<select v-model="newPriority">でセレクトボックスを双方向バインディングv-modelは数値として受け取るために:value="1"のように:をつける(またはNumber()で変換)- 色の切り替えには
:styleバインディングを使う:
:style="{ color: task.priority === 1 ? 'red' : task.priority === 3 ? 'gray' : 'black' }"
- PATCH の
bodyにpriority: task.priorityを追加する
ドリル 4: onMounted + fetch + チェックボックス + 動的スタイル - フルスタック Todo を白紙から
ドリル名: 04-build-fullstack-todo
> 🛠️ 【タイプ: ゼロベース実装問題】 ほぼ空の App.vue から仕様を読んで自力で実装してください。<template> 構造・id/class・内部ロジックは完全に自由。ただしテストが見る 画面の表示テキスト・ボタンの動作 は仕様どおりに揃えること。
問題
Vue + FastAPI (HTTP fetch 経由) で動くフルスタック Todo アプリを 白紙から 実装してください。
App.vue には最小スケルトンしかありません。<template> 構造・id/class・関数名・内部ロジック・データ取得タイミングはすべて自由ですが、下記の画面仕様を満たすこと。
⚠️ このドリルは 実務応用のチーム開発演習の予行演習 です。今までの 5 drill (fill / add) の集大成として、自分で組み立てる経験をしてください。
仕様
バックエンド API (FastAPI / http://localhost:8000)
すでに動作しているものとします。受講生は API を叩く Vue 側だけ実装。
| メソッド | URL | レスポンス例 |
|---|---|---|
| GET | /tasks | [{ "id": 1, "title": "牛乳", "done": false }, ...] |
| POST | /tasks (body: {title}) | { "id": 2, "title": "卵", "done": false } |
| PATCH | /tasks/{id} (body: {title?, done?}) | 更新後のタスク |
| DELETE | /tasks/{id} | 204 |
Vue 画面要件
- 初期表示: マウント時に
GET /tasksを呼んでタスク一覧を表示 - タスク追加: テキスト入力 + 「追加」ボタンで
POST /tasks、画面を最新化 - 完了切替: 各タスクの チェックボックス で
PATCH /tasks/{id}(done を反転) - タスク削除: 各タスクの「削除」ボタンで
DELETE /tasks/{id} - 完了状態の表示: done=true のタスクはタイトルに 取り消し線 を引く
守る (テストが見ている)
- マウント時に
fetchが呼ばれる (URL に/tasksを含む) <input>と「追加」ボタンがある- 「追加」クリックで
fetchが POST メソッドで呼ばれる - 各タスクごとに「削除」ボタンが表示される
- 各タスクごとにチェックボックスがある
確認方法
- 起動.bat / 起動.sh をダブルクリック
- ブラウザにテスト結果が表示される
自由に設計できる例
refで管理する /reactiveで管理する- onMounted で fetch する / 手動で初期化関数を呼ぶ
- ID/class は付けても付けなくてもOK
- 関数名は自由 (
loadTasks/fetchAll/getTasks...) - テンプレート構造は自由 (
<ul><li>でも<table>でも<div>でも)
> ★ 実務応用のチーム開発演習では、これと同じレベルの「ゼロから組み立てる」作業をチームで行います。
> このドリルで「自分で構造を考える」感覚を掴んでおいてください。
このドリルで学ぶこと
| 学習要素 | 学習資料との対応 |
|---|---|
onMounted で初期データ取得 | ch04 学習目標 |
| GET/POST/PATCH/DELETE を fetch で叩く | ch06 学習目標 |
| チェックボックス + PATCH 連動 | ch06 slide「チェックボックスと状態の同期」 |
動的スタイル (:style) で完了状態を可視化 | ch06 slide「動的スタイルで状態を表す」 |
| 配列削除 + 再取得で UI を最新化 | ch05 slide「リストから項目を削除する」 |
| 画面 ↔ API の双方向通信を自分で設計 | 実務応用のチーム開発演習の予行演習 |
このドリルは 実務応用のチーム開発演習の個人版予行演習。今までの 5 drill (fill / add) の集大成として、自分でデータフロー全体を設計します。
ドリル 4: ヒント
ドリル名: 04-build-fullstack-todo
> ⚠️ 自力で考えてから読んでください
段階1: データを ref で持ち、マウント時に fetch
import { ref, onMounted } from 'vue'
const tasks = ref([])
async function loadTasks() {
const res = await fetch('http://localhost:8000/tasks')
tasks.value = await res.json()
}
onMounted(loadTasks)段階2: 入力欄 + 「追加」ボタン
const newTitle = ref('')
async function addTask() {
await fetch('http://localhost:8000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle.value }),
})
// 追加後は loadTasks() で再取得
}段階3: v-for + チェックボックス + 削除
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" :checked="task.done" @change="toggleDone(task)" />
<span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
{{ task.title }}
</span>
<button @click="deleteTask(task.id)">削除</button>
</li>段階4: 完了切替
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: 'PATCH',
body: JSON.stringify({ done: !task.done }),
headers: { 'Content-Type': 'application/json' },
})
await loadTasks()
}考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
ch06-fastapi 演習ガイド
受講生向け演習問題集
採点の仕組み (読んでから始めてください)
採点テストは 「お題の通りに動くか」 を機械的にチェックします。
ドリルごとに 3つのタイプ があるので、各ドリル冒頭の 【タイプ】 を確認してください。
タイプ fill (穴埋め問題) — 文法・APIを知っているか
___(3個並んだアンダースコア) の空欄を埋める- 既存の
<template>のテキストやボタン順は そのまま - ロジック自由度: 低 (お題どおりに直す)
タイプ add (機能追加問題) — 既存コードに機能を足せるか
- 既存コードを残したまま、新しい機能を 追加 する
- 実装方法は自由 (関数名・変数名・内部ロジックは好きに)
- ただしテストが要求する見た目 (表示テキスト・ボタンの並び順) は守る
- 既存テストを壊さないこと
タイプ build (ゼロベース実装問題) — 仕様から実装を組み立てられるか
- ほぼ空の
App.vueから 自力で全部書く <template>構造・id/class・内部ロジック: 完全に自由- 守るのは画面の機能仕様だけ: 表示するテキスト・ボタンの操作で何が起きるか
- 一番ハードだが、実装力が一番つくタイプ
3タイプ共通で守ること
- ファイル名 (
App.vue) は変えない tests/フォルダは触らない (採点ロジック)- テンプレートの 表示テキスト (「カウント:」等) はテストが見ているので残す
このガイドのドリル一覧
| # | ドリル名 | タイトル | タイプ | 難易度 |
|---|---|---|---|---|
| 1 | 01-fill-patch-delete | fetch PATCH/DELETE - CRUD 4動詞の連携 | 🔧 穴埋め | ★ |
| 2 | 02-add-filter | computed + filter - リスト絞り込み | ➕ 機能追加 | ★ |
| 3 | 03-add-priority | select + 動的バインド - 優先度フィールド追加 | ➕ 機能追加 | ★ |
| 4 | 04-build-fullstack-todo | onMounted + fetch + チェックボックス + 動的スタイル - フルスタック Todo を白紙から | 🛠️ ゼロ実装 | ★★★ |
> 詰まったら ヒント ページを開く。それでも分からなければ講師に質問。
ドリル 1: fetch PATCH/DELETE - CRUD 4動詞の連携
ドリル名: 01-fill-patch-delete
> 🔧 【タイプ: 穴埋め問題】 ___ の部分を埋めて完成させてください。既存のコード構造は変えないこと。
問題
___ を埋めて、完了切り替え(PATCH)と削除(DELETE)を FastAPI と連携させよ。___ は全部で4箇所ある。
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: '___', <!-- ___1: 更新に使う HTTP メソッド -->
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: task.title, done: !task.done }),
})
await ___() <!-- ___2: 画面を最新状態に更新する関数 -->
}
async function deleteTask(id) {
await fetch(`http://localhost:8000/tasks/${___}`, { <!-- ___3: URL に埋め込む変数名 -->
method: '___', <!-- ___4: 削除に使う HTTP メソッド -->
})
await loadTasks()
}確認方法
POST /tasksでタスクを2件追加する- チェックボックスをクリックすると
doneが反転してtasks.jsonに反映される - 「削除」ボタンをクリックするとリストと
tasks.jsonから消える
ドリル 1: ヒント
ドリル名: 01-fill-patch-delete
> ⚠️ 自力で考えてから読んでください
___1: 既存リソースの部分更新に使う HTTP メソッド(PUT は全体置き換え)___2:onMountedで呼んでいる「一覧を取得して画面に反映する関数」と同じもの___3:deleteTask(id)で受け取った引数をそのまま URL に入れる___4: リソースを削除する HTTP メソッド
ドリル 2: computed + filter - リスト絞り込み
ドリル名: 02-add-filter
> ➕ 【タイプ: 機能追加問題】 既存コードを残したまま新しい機能を追加してください。実装は自由ですが、テストが要求する I/F・表示テキストは守ること。
ベースコード(base/App.vue)は GET/POST/PATCH/DELETE がすべて動く状態。
以下の機能を追加せよ。
追加する機能
- フィルタボタン: 「すべて」「未完了のみ」「完了のみ」の3つのボタンを追加する
- 絞り込み表示: ボタンの状態に応じて表示するタスクを絞り込む
- 件数表示: 「表示中 X 件 / 全 Y 件」を画面下部に表示する
ヒント
computedを使ってtasksから絞り込んだ配列を派生させる- フィルタ状態は
ref('all')のような文字列で管理する('all'/'active'/'done') v-forはtasksの代わりに computed の配列を使う
確認方法
- タスクを追加して一部を完了にする
- 「未完了のみ」で未完了タスクだけ表示される
- 「完了のみ」で完了タスクだけ表示される
- 件数が正しく変わる
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
- 現在選択中のフィルタボタンに
activeクラスを付け、色を変えて「いま何で絞り込み中か」を視覚的に分かりやすくする - タイトル検索用の入力欄を追加し、
computed内でフィルタ条件 + 文字列部分一致の AND で絞り込む - フィルタ状態 (
'all'/'active'/'done') をlocalStorageに保存して、リロード後も最後に選んだフィルタが復元されるようにする
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ドリル 2: ヒント
ドリル名: 02-add-filter
> ⚠️ 自力で考えてから読んでください
> {{HINT}}
ドリル 3: select + 動的バインド - 優先度フィールド追加
ドリル名: 03-add-priority
> ➕ 【タイプ: 機能追加問題】 既存コードを残したまま新しい機能を追加してください。実装は自由ですが、テストが要求する I/F・表示テキストは守ること。
base/ には FastAPI(main.py)と Vue(App.vue)が入っている。
バックエンドとフロントエンドの両方を改造して、タスクに優先度を追加せよ。
追加する仕様
FastAPI 側(main.py)
POST /tasksのボディにpriorityを追加する(型:int、範囲: 1〜3、デフォルト: 2)PATCH /tasks/{task_id}でもpriorityを更新できるようにするpriorityが 1〜3 以外のとき400 Bad Requestを返す
Vue 側(App.vue)
- タスク追加フォームに「優先度」の
<select>を追加する(選択肢: 1・2・3) - 各タスクの表示に priority に応じた色をつける
- 1(高): 赤(color: red)
- 2(中): 黒(デフォルト)
- 3(低): グレー(color: gray)
- PATCH 時も
priorityを送る
確認方法
priority: 1でタスクを追加 → タイトルが赤で表示されるpriority: 3でタスクを追加 → タイトルがグレーで表示されるPOST /tasksに{"title": "test", "priority": 5}を送る → 400 が返る- Swagger UI で tasks.json を確認して
priorityが保存されていること
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
- Vue 側でタスク一覧を
priority昇順で並び替えて表示する (computedでsort()) - 優先度ごとの件数 ("高: 2件 / 中: 5件 / 低: 1件") をフッタに表示する
- 各タスクの背景色や左ボーダー色も priority に応じて変え、色だけでなくレイアウトでも優先度が伝わるようにする
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ドリル 3: ヒント
ドリル名: 03-add-priority
> ⚠️ 自力で考えてから読んでください
FastAPI 側
body.get("priority", 2)でデフォルト値 2 を設定できる- バリデーション:
if priority not in (1, 2, 3): raise HTTPException(400, ...) new_taskの dict に"priority": priorityを追加する- PATCH も同様に
priorityをt.update(...)の中に含める
Vue 側
newPriority = ref(2)で優先度の状態を管理する<select v-model="newPriority">でセレクトボックスを双方向バインディングv-modelは数値として受け取るために:value="1"のように:をつける(またはNumber()で変換)- 色の切り替えには
:styleバインディングを使う:
:style="{ color: task.priority === 1 ? 'red' : task.priority === 3 ? 'gray' : 'black' }"
- PATCH の
bodyにpriority: task.priorityを追加する
ドリル 4: onMounted + fetch + チェックボックス + 動的スタイル - フルスタック Todo を白紙から
ドリル名: 04-build-fullstack-todo
> 🛠️ 【タイプ: ゼロベース実装問題】 ほぼ空の App.vue から仕様を読んで自力で実装してください。<template> 構造・id/class・内部ロジックは完全に自由。ただしテストが見る 画面の表示テキスト・ボタンの動作 は仕様どおりに揃えること。
問題
Vue + FastAPI (HTTP fetch 経由) で動くフルスタック Todo アプリを 白紙から 実装してください。
App.vue には最小スケルトンしかありません。<template> 構造・id/class・関数名・内部ロジック・データ取得タイミングはすべて自由ですが、下記の画面仕様を満たすこと。
⚠️ このドリルは 実務応用のチーム開発演習の予行演習 です。今までの 5 drill (fill / add) の集大成として、自分で組み立てる経験をしてください。
仕様
バックエンド API (FastAPI / http://localhost:8000)
すでに動作しているものとします。受講生は API を叩く Vue 側だけ実装。
| メソッド | URL | レスポンス例 |
|---|---|---|
| GET | /tasks | [{ "id": 1, "title": "牛乳", "done": false }, ...] |
| POST | /tasks (body: {title}) | { "id": 2, "title": "卵", "done": false } |
| PATCH | /tasks/{id} (body: {title?, done?}) | 更新後のタスク |
| DELETE | /tasks/{id} | 204 |
Vue 画面要件
- 初期表示: マウント時に
GET /tasksを呼んでタスク一覧を表示 - タスク追加: テキスト入力 + 「追加」ボタンで
POST /tasks、画面を最新化 - 完了切替: 各タスクの チェックボックス で
PATCH /tasks/{id}(done を反転) - タスク削除: 各タスクの「削除」ボタンで
DELETE /tasks/{id} - 完了状態の表示: done=true のタスクはタイトルに 取り消し線 を引く
守る (テストが見ている)
- マウント時に
fetchが呼ばれる (URL に/tasksを含む) <input>と「追加」ボタンがある- 「追加」クリックで
fetchが POST メソッドで呼ばれる - 各タスクごとに「削除」ボタンが表示される
- 各タスクごとにチェックボックスがある
確認方法
- 起動.bat / 起動.sh をダブルクリック
- ブラウザにテスト結果が表示される
自由に設計できる例
refで管理する /reactiveで管理する- onMounted で fetch する / 手動で初期化関数を呼ぶ
- ID/class は付けても付けなくてもOK
- 関数名は自由 (
loadTasks/fetchAll/getTasks...) - テンプレート構造は自由 (
<ul><li>でも<table>でも<div>でも)
> ★ 実務応用のチーム開発演習では、これと同じレベルの「ゼロから組み立てる」作業をチームで行います。
> このドリルで「自分で構造を考える」感覚を掴んでおいてください。
このドリルで学ぶこと
| 学習要素 | 学習資料との対応 |
|---|---|
onMounted で初期データ取得 | ch04 学習目標 |
| GET/POST/PATCH/DELETE を fetch で叩く | ch06 学習目標 |
| チェックボックス + PATCH 連動 | ch06 slide「チェックボックスと状態の同期」 |
動的スタイル (:style) で完了状態を可視化 | ch06 slide「動的スタイルで状態を表す」 |
| 配列削除 + 再取得で UI を最新化 | ch05 slide「リストから項目を削除する」 |
| 画面 ↔ API の双方向通信を自分で設計 | 実務応用のチーム開発演習の予行演習 |
このドリルは 実務応用のチーム開発演習の個人版予行演習。今までの 5 drill (fill / add) の集大成として、自分でデータフロー全体を設計します。
ドリル 4: ヒント
ドリル名: 04-build-fullstack-todo
> ⚠️ 自力で考えてから読んでください
段階1: データを ref で持ち、マウント時に fetch
import { ref, onMounted } from 'vue'
const tasks = ref([])
async function loadTasks() {
const res = await fetch('http://localhost:8000/tasks')
tasks.value = await res.json()
}
onMounted(loadTasks)段階2: 入力欄 + 「追加」ボタン
const newTitle = ref('')
async function addTask() {
await fetch('http://localhost:8000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle.value }),
})
// 追加後は loadTasks() で再取得
}段階3: v-for + チェックボックス + 削除
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" :checked="task.done" @change="toggleDone(task)" />
<span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
{{ task.title }}
</span>
<button @click="deleteTask(task.id)">削除</button>
</li>段階4: 完了切替
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: 'PATCH',
body: JSON.stringify({ done: !task.done }),
headers: { 'Content-Type': 'application/json' },
})
await loadTasks()
}ch06-fastapi 演習解説
⚠️ 自力でドリルを解いてから読んでください
ドリル 1: fetch PATCH/DELETE - CRUD 4動詞の連携 — 模範解答
ドリル名: 01-fill-patch-delete
<script setup>
import { ref, onMounted } from 'vue'
const newTitle = ref('')
const tasks = ref([])
async function loadTasks() {
const res = await fetch('http://localhost:8000/tasks')
tasks.value = await res.json()
}
async function addTask() {
if (!newTitle.value.trim()) return
await fetch('http://localhost:8000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle.value }),
})
newTitle.value = ''
await loadTasks()
}
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: task.title, done: !task.done }),
})
await loadTasks()
}
async function deleteTask(id) {
await fetch(`http://localhost:8000/tasks/${id}`, {
method: 'DELETE',
})
await loadTasks()
}
onMounted(loadTasks)
</script>
<template>
<h1>タスク一覧</h1>
<input v-model="newTitle" placeholder="タスク名を入力" />
<button @click="addTask">追加</button>
<ul>
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" :checked="task.done" @change="toggleDone(task)" />
<span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
{{ task.title }}
</span>
<button @click="deleteTask(task.id)">削除</button>
</li>
</ul>
</template>ドリル 1: fetch PATCH/DELETE - CRUD 4動詞の連携 — 解説
ドリル名: 01-fill-patch-delete
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: task.title, done: !task.done }),
})
await loadTasks()
}
async function deleteTask(id) {
await fetch(`http://localhost:8000/tasks/${id}`, {
method: 'DELETE',
})
await loadTasks()
}___1→'PATCH'___2→loadTasks___3→id___4→'DELETE'
ドリル 2: computed + filter - リスト絞り込み — 模範解答
ドリル名: 02-add-filter
<script setup>
import { ref, computed, onMounted } from 'vue'
const newTitle = ref('')
const tasks = ref([])
const filter = ref('all')
async function loadTasks() {
const res = await fetch('http://localhost:8000/tasks')
tasks.value = await res.json()
}
async function addTask() {
if (!newTitle.value.trim()) return
await fetch('http://localhost:8000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle.value }),
})
newTitle.value = ''
await loadTasks()
}
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: task.title, done: !task.done }),
})
await loadTasks()
}
async function deleteTask(id) {
await fetch(`http://localhost:8000/tasks/${id}`, {
method: 'DELETE',
})
await loadTasks()
}
const filteredTasks = computed(() => {
if (filter.value === 'active') return tasks.value.filter(t => !t.done)
if (filter.value === 'done') return tasks.value.filter(t => t.done)
return tasks.value
})
onMounted(loadTasks)
</script>
<template>
<h1>タスク一覧</h1>
<input v-model="newTitle" placeholder="タスク名を入力" />
<button @click="addTask">追加</button>
<div>
<button @click="filter = 'all'">すべて</button>
<button @click="filter = 'active'">未完了のみ</button>
<button @click="filter = 'done'">完了のみ</button>
</div>
<ul>
<li v-for="task in filteredTasks" :key="task.id">
<input type="checkbox" :checked="task.done" @change="toggleDone(task)" />
<span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
{{ task.title }}
</span>
<button @click="deleteTask(task.id)">削除</button>
</li>
</ul>
<p>表示中 {{ filteredTasks.length }} 件 / 全 {{ tasks.length }} 件</p>
</template>ドリル 2: computed + filter - リスト絞り込み — 解説
ドリル名: 02-add-filter
{{ANSWER_CODE}}ドリル 3: select + 動的バインド - 優先度フィールド追加 — 模範解答
ドリル名: 03-add-priority
<script setup>
import { ref, onMounted } from 'vue'
const newTitle = ref('')
const newPriority = ref(2)
const tasks = ref([])
async function loadTasks() {
const res = await fetch('http://localhost:8000/tasks')
tasks.value = await res.json()
}
async function addTask() {
if (!newTitle.value.trim()) return
await fetch('http://localhost:8000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle.value, priority: Number(newPriority.value) }),
})
newTitle.value = ''
await loadTasks()
}
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: task.title, done: !task.done, priority: task.priority }),
})
await loadTasks()
}
async function deleteTask(id) {
await fetch(`http://localhost:8000/tasks/${id}`, {
method: 'DELETE',
})
await loadTasks()
}
function priorityColor(priority) {
if (priority === 1) return 'red'
if (priority === 3) return 'gray'
return 'black'
}
onMounted(loadTasks)
</script>
<template>
<h1>タスク一覧</h1>
<input v-model="newTitle" placeholder="タスク名を入力" />
<select v-model="newPriority">
<option :value="1">1(高)</option>
<option :value="2">2(中)</option>
<option :value="3">3(低)</option>
</select>
<button @click="addTask">追加</button>
<ul>
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" :checked="task.done" @change="toggleDone(task)" />
<span :style="{ textDecoration: task.done ? 'line-through' : 'none', color: priorityColor(task.priority) }">
{{ task.title }}
</span>
<button @click="deleteTask(task.id)">削除</button>
</li>
</ul>
</template>ドリル 3: select + 動的バインド - 優先度フィールド追加 — 解説
ドリル名: 03-add-priority
FastAPI 側のポイント
@app.post("/tasks", status_code=201)
async def create_task(request: Request):
body = await request.json()
priority = body.get("priority", 2) # デフォルト 2
if priority not in (1, 2, 3):
raise HTTPException(status_code=400, detail="priority must be 1, 2, or 3")
...
new_task = {
"id": ..., "title": ..., "done": ...,
"priority": priority, # dict に追加
}PATCH も同様。"priority" in body で省略されたリクエストを通す。
Vue 側のポイント
:value="1" と v-model の組み合わせ
<select v-model="newPriority"> <option :value="1">1(高)</option> <option :value="2">2(中)</option> <option :value="3">3(低)</option> </select>
:value で数値バインディング。v-model="newPriority" にすると選択した値が数値として newPriority に入る。ただし <select> の初期値は文字列として扱われる場合があるため、POST 時に Number(newPriority.value) で明示的に変換する。
色の切り替え
function priorityColor(priority) {
if (priority === 1) return 'red'
if (priority === 3) return 'gray'
return 'black'
}テンプレートにロジックを埋め込むより関数に切り出した方が読みやすい。
設計上の選択肢
priorityColorを computed で書く方法もある(tasksが多い場合は computed の方がキャッシュ効率が良い)- PATCH 時に
priorityを省略可能にするか必須にするかは要件次第。今回は省略可能にした
チェックポイント(自問自答用)
- バックエンドを変えると既存の
tasks.json(priorityなし)との互換性はどうなる? priorityフィールドがない古いレコードが混在したとき、Vue 側でundefinedが来たらどう表示される?
ドリル 4: onMounted + fetch + チェックボックス + 動的スタイル - フルスタック Todo を白紙から — 模範解答
ドリル名: 04-build-fullstack-todo
<script setup>
import { ref, onMounted } from 'vue'
const API = 'http://localhost:8000'
const newTitle = ref('')
const tasks = ref([])
async function loadTasks() {
const res = await fetch(`${API}/tasks`)
tasks.value = await res.json()
}
async function addTask() {
if (!newTitle.value.trim()) return
await fetch(`${API}/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle.value }),
})
newTitle.value = ''
await loadTasks()
}
async function toggleDone(task) {
await fetch(`${API}/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ done: !task.done }),
})
await loadTasks()
}
async function deleteTask(id) {
await fetch(`${API}/tasks/${id}`, { method: 'DELETE' })
await loadTasks()
}
onMounted(loadTasks)
</script>
<template>
<div>
<h1>Todo</h1>
<input v-model="newTitle" type="text" placeholder="タスク名" />
<button @click="addTask">追加</button>
<ul>
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" :checked="task.done" @change="toggleDone(task)" />
<span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
{{ task.title }}
</span>
<button @click="deleteTask(task.id)">削除</button>
</li>
</ul>
</div>
</template>ドリル 4: onMounted + fetch + チェックボックス + 動的スタイル - フルスタック Todo を白紙から — 解説
ドリル名: 04-build-fullstack-todo
フルスタック実装の思考プロセス
- データの流れを設計する — UI ↔ Vue state ↔ HTTP API ↔ FastAPI
- マウント時の初期化 —
onMountedで API 呼び出し - ユーザー操作 → API 呼び出し → state 更新 — 一連の流れを関数化
- エラーハンドリング — 本ドリルでは省略可、実務では
try/catchで UI に伝える
「楽観的UI」 vs 「サーバー再取得」
このドリルでは サーバー再取得 (loadTasks()) 方式を採用:
| パターン | 説明 | 利点 |
|---|---|---|
| サーバー再取得 | 操作後に GET をもう一度叩いて全件取得 | シンプル、データのズレが出にくい |
| 楽観的UI | API レスポンスを待たず state を先に更新 | UX が速い、ネットワーク遅延でも快適 |
スタートは「サーバー再取得」、慣れたら「楽観的UI」に書き換えてみると深い学習に。
実装が複数あり得る点
- 状態管理:
ref/reactive/ Pinia (このドリルでは Pinia は不要) - fetch のラッパー: 関数毎に直接書く / 共通の
api.jsを作る - 完了状態の見せ方: 取り消し線 / 文字色変更 / アイコン
- 削除確認ダイアログ: あり / なし
Q: PATCH / DELETE の空欄を埋める
問題
___ を埋めて、完了切り替え(PATCH)と削除(DELETE)を FastAPI と連携させよ。___ は全部で4箇所ある。
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: '___', <!-- ___1: 更新に使う HTTP メソッド -->
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: task.title, done: !task.done }),
})
await ___() <!-- ___2: 画面を最新状態に更新する関数 -->
}
async function deleteTask(id) {
await fetch(`http://localhost:8000/tasks/${___}`, { <!-- ___3: URL に埋め込む変数名 -->
method: '___', <!-- ___4: 削除に使う HTTP メソッド -->
})
await loadTasks()
}確認方法
POST /tasksでタスクを2件追加する- チェックボックスをクリックすると
doneが反転してtasks.jsonに反映される - 「削除」ボタンをクリックするとリストと
tasks.jsonから消える
ヒント: PATCH / DELETE の空欄
___1: 既存リソースの部分更新に使う HTTP メソッド(PUT は全体置き換え)___2:onMountedで呼んでいる「一覧を取得して画面に反映する関数」と同じもの___3:deleteTask(id)で受け取った引数をそのまま URL に入れる___4: リソースを削除する HTTP メソッド
解答: PATCH / DELETE の空欄
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: task.title, done: !task.done }),
})
await loadTasks()
}
async function deleteTask(id) {
await fetch(`http://localhost:8000/tasks/${id}`, {
method: 'DELETE',
})
await loadTasks()
}___1→'PATCH'___2→loadTasks___3→id___4→'DELETE'
演習: 絞り込みフィルタを追加する
ベースコード(base/App.vue)は GET/POST/PATCH/DELETE がすべて動く状態。
以下の機能を追加せよ。
追加する機能
- フィルタボタン: 「すべて」「未完了のみ」「完了のみ」の3つのボタンを追加する
- 絞り込み表示: ボタンの状態に応じて表示するタスクを絞り込む
- 件数表示: 「表示中 X 件 / 全 Y 件」を画面下部に表示する
ヒント
computedを使ってtasksから絞り込んだ配列を派生させる- フィルタ状態は
ref('all')のような文字列で管理する('all'/'active'/'done') v-forはtasksの代わりに computed の配列を使う
確認方法
- タスクを追加して一部を完了にする
- 「未完了のみ」で未完了タスクだけ表示される
- 「完了のみ」で完了タスクだけ表示される
- 件数が正しく変わる
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
- 現在選択中のフィルタボタンに
activeクラスを付け、色を変えて「いま何で絞り込み中か」を視覚的に分かりやすくする - タイトル検索用の入力欄を追加し、
computed内でフィルタ条件 + 文字列部分一致の AND で絞り込む - フィルタ状態 (
'all'/'active'/'done') をlocalStorageに保存して、リロード後も最後に選んだフィルタが復元されるようにする
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
> {{HINT}}
解答
{{ANSWER_CODE}}演習: 優先度付きタスク管理
base/ には FastAPI(main.py)と Vue(App.vue)が入っている。
バックエンドとフロントエンドの両方を改造して、タスクに優先度を追加せよ。
追加する仕様
FastAPI 側(main.py)
POST /tasksのボディにpriorityを追加する(型:int、範囲: 1〜3、デフォルト: 2)PATCH /tasks/{task_id}でもpriorityを更新できるようにするpriorityが 1〜3 以外のとき400 Bad Requestを返す
Vue 側(App.vue)
- タスク追加フォームに「優先度」の
<select>を追加する(選択肢: 1・2・3) - 各タスクの表示に priority に応じた色をつける
- 1(高): 赤(color: red)
- 2(中): 黒(デフォルト)
- 3(低): グレー(color: gray)
- PATCH 時も
priorityを送る
確認方法
priority: 1でタスクを追加 → タイトルが赤で表示されるpriority: 3でタスクを追加 → タイトルがグレーで表示されるPOST /tasksに{"title": "test", "priority": 5}を送る → 400 が返る- Swagger UI で tasks.json を確認して
priorityが保存されていること
★ 余裕がある人向け: 拡張課題
このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。
- Vue 側でタスク一覧を
priority昇順で並び替えて表示する (computedでsort()) - 優先度ごとの件数 ("高: 2件 / 中: 5件 / 低: 1件") をフッタに表示する
- 各タスクの背景色や左ボーダー色も priority に応じて変え、色だけでなくレイアウトでも優先度が伝わるようにする
採点ロジックは元のテストのままです。拡張部分を実装してもテストは PASS のまま、自分の力試しになります。
ヒント
FastAPI 側
body.get("priority", 2)でデフォルト値 2 を設定できる- バリデーション:
if priority not in (1, 2, 3): raise HTTPException(400, ...) new_taskの dict に"priority": priorityを追加する- PATCH も同様に
priorityをt.update(...)の中に含める
Vue 側
newPriority = ref(2)で優先度の状態を管理する<select v-model="newPriority">でセレクトボックスを双方向バインディングv-modelは数値として受け取るために:value="1"のように:をつける(またはNumber()で変換)- 色の切り替えには
:styleバインディングを使う:
:style="{ color: task.priority === 1 ? 'red' : task.priority === 3 ? 'gray' : 'black' }"
- PATCH の
bodyにpriority: task.priorityを追加する
解答
FastAPI 側のポイント
@app.post("/tasks", status_code=201)
async def create_task(request: Request):
body = await request.json()
priority = body.get("priority", 2) # デフォルト 2
if priority not in (1, 2, 3):
raise HTTPException(status_code=400, detail="priority must be 1, 2, or 3")
...
new_task = {
"id": ..., "title": ..., "done": ...,
"priority": priority, # dict に追加
}PATCH も同様。"priority" in body で省略されたリクエストを通す。
Vue 側のポイント
:value="1" と v-model の組み合わせ
<select v-model="newPriority"> <option :value="1">1(高)</option> <option :value="2">2(中)</option> <option :value="3">3(低)</option> </select>
:value で数値バインディング。v-model="newPriority" にすると選択した値が数値として newPriority に入る。ただし <select> の初期値は文字列として扱われる場合があるため、POST 時に Number(newPriority.value) で明示的に変換する。
色の切り替え
function priorityColor(priority) {
if (priority === 1) return 'red'
if (priority === 3) return 'gray'
return 'black'
}テンプレートにロジックを埋め込むより関数に切り出した方が読みやすい。
設計上の選択肢
priorityColorを computed で書く方法もある(tasksが多い場合は computed の方がキャッシュ効率が良い)- PATCH 時に
priorityを省略可能にするか必須にするかは要件次第。今回は省略可能にした
チェックポイント(自問自答用)
- バックエンドを変えると既存の
tasks.json(priorityなし)との互換性はどうなる? priorityフィールドがない古いレコードが混在したとき、Vue 側でundefinedが来たらどう表示される?
Q: フルスタック Todo アプリを ゼロから実装する
問題
Vue + FastAPI (HTTP fetch 経由) で動くフルスタック Todo アプリを 白紙から 実装してください。
App.vue には最小スケルトンしかありません。<template> 構造・id/class・関数名・内部ロジック・データ取得タイミングはすべて自由ですが、下記の画面仕様を満たすこと。
⚠️ このドリルは 実務応用のチーム開発演習の予行演習 です。今までの 5 drill (fill / add) の集大成として、自分で組み立てる経験をしてください。
仕様
バックエンド API (FastAPI / http://localhost:8000)
すでに動作しているものとします。受講生は API を叩く Vue 側だけ実装。
| メソッド | URL | レスポンス例 |
|---|---|---|
| GET | /tasks | [{ "id": 1, "title": "牛乳", "done": false }, ...] |
| POST | /tasks (body: {title}) | { "id": 2, "title": "卵", "done": false } |
| PATCH | /tasks/{id} (body: {title?, done?}) | 更新後のタスク |
| DELETE | /tasks/{id} | 204 |
Vue 画面要件
- 初期表示: マウント時に
GET /tasksを呼んでタスク一覧を表示 - タスク追加: テキスト入力 + 「追加」ボタンで
POST /tasks、画面を最新化 - 完了切替: 各タスクの チェックボックス で
PATCH /tasks/{id}(done を反転) - タスク削除: 各タスクの「削除」ボタンで
DELETE /tasks/{id} - 完了状態の表示: done=true のタスクはタイトルに 取り消し線 を引く
守る (テストが見ている)
- マウント時に
fetchが呼ばれる (URL に/tasksを含む) <input>と「追加」ボタンがある- 「追加」クリックで
fetchが POST メソッドで呼ばれる - 各タスクごとに「削除」ボタンが表示される
- 各タスクごとにチェックボックスがある
確認方法
- 起動.bat / 起動.sh をダブルクリック
- ブラウザにテスト結果が表示される
自由に設計できる例
refで管理する /reactiveで管理する- onMounted で fetch する / 手動で初期化関数を呼ぶ
- ID/class は付けても付けなくてもOK
- 関数名は自由 (
loadTasks/fetchAll/getTasks...) - テンプレート構造は自由 (
<ul><li>でも<table>でも<div>でも)
> ★ 実務応用のチーム開発演習では、これと同じレベルの「ゼロから組み立てる」作業をチームで行います。
> このドリルで「自分で構造を考える」感覚を掴んでおいてください。
このドリルで学ぶこと
| 学習要素 | 学習資料との対応 |
|---|---|
onMounted で初期データ取得 | ch04 学習目標 |
| GET/POST/PATCH/DELETE を fetch で叩く | ch06 学習目標 |
| チェックボックス + PATCH 連動 | ch06 slide「チェックボックスと状態の同期」 |
動的スタイル (:style) で完了状態を可視化 | ch06 slide「動的スタイルで状態を表す」 |
| 配列削除 + 再取得で UI を最新化 | ch05 slide「リストから項目を削除する」 |
| 画面 ↔ API の双方向通信を自分で設計 | 実務応用のチーム開発演習の予行演習 |
このドリルは 実務応用のチーム開発演習の個人版予行演習。今までの 5 drill (fill / add) の集大成として、自分でデータフロー全体を設計します。
ヒント
段階1: データを ref で持ち、マウント時に fetch
import { ref, onMounted } from 'vue'
const tasks = ref([])
async function loadTasks() {
const res = await fetch('http://localhost:8000/tasks')
tasks.value = await res.json()
}
onMounted(loadTasks)段階2: 入力欄 + 「追加」ボタン
const newTitle = ref('')
async function addTask() {
await fetch('http://localhost:8000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle.value }),
})
// 追加後は loadTasks() で再取得
}段階3: v-for + チェックボックス + 削除
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" :checked="task.done" @change="toggleDone(task)" />
<span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
{{ task.title }}
</span>
<button @click="deleteTask(task.id)">削除</button>
</li>段階4: 完了切替
async function toggleDone(task) {
await fetch(`http://localhost:8000/tasks/${task.id}`, {
method: 'PATCH',
body: JSON.stringify({ done: !task.done }),
headers: { 'Content-Type': 'application/json' },
})
await loadTasks()
}解説
フルスタック実装の思考プロセス
- データの流れを設計する — UI ↔ Vue state ↔ HTTP API ↔ FastAPI
- マウント時の初期化 —
onMountedで API 呼び出し - ユーザー操作 → API 呼び出し → state 更新 — 一連の流れを関数化
- エラーハンドリング — 本ドリルでは省略可、実務では
try/catchで UI に伝える
「楽観的UI」 vs 「サーバー再取得」
このドリルでは サーバー再取得 (loadTasks()) 方式を採用:
| パターン | 説明 | 利点 |
|---|---|---|
| サーバー再取得 | 操作後に GET をもう一度叩いて全件取得 | シンプル、データのズレが出にくい |
| 楽観的UI | API レスポンスを待たず state を先に更新 | UX が速い、ネットワーク遅延でも快適 |
スタートは「サーバー再取得」、慣れたら「楽観的UI」に書き換えてみると深い学習に。
実装が複数あり得る点
- 状態管理:
ref/reactive/ Pinia (このドリルでは Pinia は不要) - fetch のラッパー: 関数毎に直接書く / 共通の
api.jsを作る - 完了状態の見せ方: 取り消し線 / 文字色変更 / アイコン
- 削除確認ダイアログ: あり / なし
演習: テキスト検索フィルタを追加する
タスク一覧の上にテキスト入力を置き、 入力した文字列を タイトルに含むタスクだけ表示 するように実装せよ。
やること
searchQueryという ref を追加 (v-modelでテキスト入力にバインド)filteredTasksという computed を追加:searchQueryをtitleに含むタスクのみ返す- 大文字小文字を区別しない (Buy で "buy milk" もヒット)
searchQueryが空なら全件表示- テンプレート側で
tasks.valueの代わりにfilteredTasksを表示
確認方法
- ブラウザで
http://localhost:5173 - 「買い物」「Buy milk」「掃除」 のタスクを登録
- 検索欄に「buy」と入力 → "Buy milk" のみ表示される
- 検索欄を空にする → 全3件表示される
仕様
- バックエンド側のフィルタリング (
?q=) は使わない。 フロント側で computed で絞る - 件数表示 (例: "3 件中 1件表示中") はオプション
★ 余裕がある人向け
- バックエンド側
?q=クエリパラメータと併用 (= サーバー側で絞ってから computed で更に絞る) 設計 - 検索欄に debounce (300ms 遅延) を追加してパフォーマンス改善
- 検索ヒット件数を表示
ヒント
1. ref を追加
const searchQuery = ref('')2. computed で絞り込み
const filteredTasks = computed(() => {
if (!searchQuery.value) return tasks.value
return tasks.value.filter(t =>
t.title.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}).toLowerCase() で大文字小文字無視。
3. テンプレートで filteredTasks
<input v-model="searchQuery" placeholder="検索" /> <ul> <li v-for="task in filteredTasks" :key="task.id">
tasks を filteredTasks に差し替えるだけ。
解説
なぜ computed か
computed は 依存する ref が変わると自動再計算 + 結果をキャッシュ。
通常の関数だと renderingごとに毎回 filter が走ってしまうが、 computed なら tasks か searchQuery が変わったときだけ計算する。
フロント側 filter vs サーバー側 filter
| フロント | サーバー (FastAPI ?q=) | |
|---|---|---|
| データ量小 (~100件) | OK / 入力即時反映 | OK / 通信ロス分遅い |
| データ量大 (1万件+) | 全件取得が重い | OK / 必要分だけ取得 |
| ネットワークオフライン | 動く | 動かない |
実務では「初回ロード時に全件 → フロント filter」 と「都度 サーバー filter」 を組み合わせる。
debounce (高度な話)
ユーザーが「a / ap / app / appl / apple」 とタイプするたびに filter が走る。 通常は問題ないが、 サーバー filter と組み合わせる場合は debounce (300ms 待ってから検索) で API 呼び出し回数を減らす。
演習: ソート順をプルダウンで切り替える
<select> プルダウンで「id 順」「priority 順」を切り替えられるようにし、 タスク一覧の表示順を変えよ。
前提
- 03-add-priority が完了済 (各 task に
priorityフィールドがある)
やること
sortByという ref を追加 ('id'/'priority')<select v-model="sortBy">で切替UI を追加sortedTasksという computed を追加: sortBy に応じて並び替え- priority は 数値昇順 (1 = 高優先 = 上)
- id は 数値昇順 (登録順)
確認方法
- priority=3 のタスクを先に登録、 priority=1 を後に登録
- プルダウンで「id 順」 → 登録順 (3→1)
- プルダウンで「priority 順」 → 1→3 の順
ヒント
sort() は破壊的なので copy を取る
const copy = [...tasks.value] copy.sort((a, b) => a[sortBy.value] - b[sortBy.value])
tasks.value.sort(...) を直接やると元配列を書き換えてしまい reactivity に影響する。 spread でコピー。
数値ソートの key
(a, b) => a.id - b.id // id 昇順 (a, b) => a.priority - b.priority // priority 昇順
文字列なら localeCompare、 数値なら引き算で OK。
sortBy で動的にアクセス
a[sortBy.value] の bracket記法で動的にフィールドアクセスできる。
解説
なぜ sort() を直接呼んではいけないか
tasks.value.sort(...) // ❌ tasks 自体が変わる
Vue 3 の reactivity は内部で proxy を介すが、 「並び替え」 だけでも書き換え判定が走り、 余計な再描画が起きる。 computed の中では 元データを保護 するのが定石。
ソート方向 (asc / desc)
- 昇順:
(a, b) => a.key - b.key - 降順:
(a, b) => b.key - a.key(a, b を入れ替え)
将来 ?order=desc を加えるときも同じ書式で拡張できる。
安定ソート
JavaScript の Array.prototype.sort は ES2019 以降で 安定ソート保証。 同じキー値の要素は元の順序を維持する。
演習: 削除前に確認ダイアログ
「削除」 ボタンを押したとき、 ブラウザ標準の window.confirm() で確認ダイアログを出し、 「OK」 を押した時のみ実際に削除するようにせよ。
やること
deleteTask関数の冒頭でwindow.confirm("削除しますか?")を呼ぶ- confirm が false を返したら早期 return (= 削除しない)
- confirm が true なら従来通り fetch DELETE
確認方法
- タスクを 3件登録
- 削除ボタンを押す → ダイアログ「削除しますか?」 が出る
- 「キャンセル」 → 削除されない
- 「OK」 → 削除される
仕様
window.confirmで十分 (= デフォルトの OK/キャンセル ボタン)- カスタムモーダルは不要 (★★★ で別 drill)
ヒント
async function deleteTask(id) {
if (!window.confirm("削除しますか?")) return
// 以下、 既存の fetch DELETE
}window.confirm は OK で true、 キャンセル で false。
解説
なぜ確認ダイアログが必要か
DELETE は不可逆操作。 誤クリックで貴重なデータを失う事故を防ぐ。
window.confirm の制約
- カスタマイズ不可 (OK / Cancel ボタンのみ、 多言語切替不可)
- 同期的 (= ブロッキング) なのでスタイリッシュではない
実務では
- カスタムモーダル (Headless UI / Vuetify / 自前) を使うのが一般的
- 削除取り消し (Undo) パターンも検討 → 「削除しました [取消]」 トースト
- ダブルクリック必須にする UI もある
本 drill は最小コストで「削除 = 確認」 のパターンを学ぶ目的。
演習: タスク名クリックで inline 編集
タスク名をクリックすると その場で input に切り替わり、 編集できるようにせよ。 Enter または blur で保存、 Escape でキャンセル。
やること
editingIdという ref を追加 (現在編集中の task id、 デフォルト null)- タスク名 span をクリックすると
editingId = task.idにして input 表示 - input の値が変わったら PATCH で保存
- Enter キーで保存 + 終了
- Escape キーで保存せずに終了 (元の値に戻す)
- blur (フォーカスが外れる) でも保存 + 終了
仕様
- v-if / v-else でテキスト表示と input 表示を切替
- 編集中の task は 1つだけ (
editingIdが単数) - 編集中に新規 task 追加・他 task 削除しても editing 状態は維持
確認方法
- タスク 3件登録
- 真ん中のタスク名をクリック → input に変わる
- 「修正後」 と入力 → Enter で確定
- ページリロード後も内容が保存されている (PATCH 成功)
ヒント
状態管理
editingId: 編集中の task id (null なら誰も編集中じゃない)editingTitle: 編集中の文字列バッファ
v-if で切替
<input v-if="editingId === task.id" v-model="editingTitle" @keyup.enter="saveEdit(task)" @blur="saveEdit(task)" />
<span v-else @click="startEdit(task)">{{ task.title }}</span>キー modifier
@keyup.enter= Enter キー@keyup.esc= Escape キー
Vue の event modifier で簡潔に書ける。
blur (フォーカスアウト) で保存
@blur イベントを使うと、 ユーザーが「クリックして他の要素に移動した」 時に自動保存。
解説
inline 編集の UX
「編集ボタンを押して別画面に行く」 より、 「その場でクリックして直す」 方が早い。 Trello, Notion 等の現代 UI で標準。
v-if vs v-show
本 drill では v-if を使った (DOM を生成/破棄)。
v-show は表示/非表示を CSS で切替。
inline edit では編集モード時のみ input が必要なので v-if で十分。
autofocus
input 要素に autofocus 属性をつけると、 描画直後に自動でフォーカスが当たる。 ユーザーがすぐタイプ開始できる。
編集中の他操作
実務では「他の task をクリックしたら現在の編集を確定/破棄するか」 等の判断が必要。 本 drill は単純化のため、 他をクリックしても影響しない仕様。
デバウンス自動保存
更に高度な UX として「タイプ中 1秒静止したら自動保存」 もある。 本 drill は Enter / blur で十分。
演習: 読み込み中状態の表示
fetch 実行中に 「読み込み中…」 を表示し、 完了したら通常の一覧表示に戻すようにせよ。
やること
loadingという ref (boolean) を追加loadTasksの冒頭でloading.value = true、 最後 (finally) でfalse- テンプレートで
v-if="loading"で「読み込み中…」 を表示 - addTask / toggleDone / deleteTask 等の他 fetch でも loading を立てる
仕様
loading中は input / button を disabled にする (連打防止)- 通常の一覧は
v-if="!loading"で隠す or そのまま表示するかは任意 - エラー時 (fetch reject) も finally で loading を解除
確認方法
- ブラウザの DevTools > Network > Throttling を "Slow 3G" に
- リロード → 「読み込み中…」 が一瞬表示される
- 追加ボタンを押す → 短時間 disabled になる
ヒント
try / finally で確実にリセット
async function loadTasks() {
loading.value = true
try {
// fetch ...
} finally {
loading.value = false
}
}finally を使うとエラーが起きても確実に loading を false に戻せる。
disabled で連打防止
<button :disabled="loading">追加</button> <input :disabled="loading" />
loading 中はユーザーが連打できないように。
v-if で表示切替
<p v-if="loading">読み込み中…</p> <ul v-if="!loading">...</ul>
解説
ローディング表示の重要性
- ユーザーは「画面が反応しないと焦って連打する」 → サーバー負荷上がる
- 「今、 何かが進んでいる」 ことを伝えるのは UX の基本
try / finally パターン
loading.value = true
try {
await heavyOperation()
} catch (e) {
// エラー処理
} finally {
loading.value = false // 確実に解除
}「成功でも失敗でも loading を解除する」 を明示する標準パターン。
グローバル loading vs 個別 loading
本 drill は全 fetch 共通の 1つの loading で UI 全体を制御。 実務では:
- 個別 loading (この task だけ削除中) → ボタン横の spinner
- グローバル loading → ページ全体の overlay
を使い分ける。
ローディング表示の見え方
- 短すぎる (~100ms) loading は表示するとちらつく → 200ms 以上の遅延で出すパターンも
- skeleton screen (= ぼやけたプレースホルダ) → 体感速度を改善する