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

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-decorationtextDecoration のように キャメルケース で書く
  • 複雑になったら :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/ フォルダは触らない (採点ロジック)
  • テンプレートの 表示テキスト (「カウント:」等) はテストが見ているので残す

このガイドのドリル一覧

#ドリル名タイトルタイプ難易度
101-fill-patch-deletefetch PATCH/DELETE - CRUD 4動詞の連携🔧 穴埋め
202-add-filtercomputed + filter - リスト絞り込み➕ 機能追加
303-add-priorityselect + 動的バインド - 優先度フィールド追加➕ 機能追加
404-build-fullstack-todoonMounted + 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()
}

確認方法

  1. POST /tasks でタスクを2件追加する
  2. チェックボックスをクリックすると done が反転して tasks.json に反映される
  3. 「削除」ボタンをクリックするとリストと 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 がすべて動く状態。

以下の機能を追加せよ。

追加する機能

  1. フィルタボタン: 「すべて」「未完了のみ」「完了のみ」の3つのボタンを追加する
  2. 絞り込み表示: ボタンの状態に応じて表示するタスクを絞り込む
  3. 件数表示: 「表示中 X 件 / 全 Y 件」を画面下部に表示する

ヒント

  • computed を使って tasks から絞り込んだ配列を派生させる
  • フィルタ状態は ref('all') のような文字列で管理する('all' / 'active' / 'done'
  • v-fortasks の代わりに computed の配列を使う

確認方法

  1. タスクを追加して一部を完了にする
  2. 「未完了のみ」で未完了タスクだけ表示される
  3. 「完了のみ」で完了タスクだけ表示される
  4. 件数が正しく変わる

★ 余裕がある人向け: 拡張課題

このドリルが 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 を送る

確認方法

  1. priority: 1 でタスクを追加 → タイトルが赤で表示される
  2. priority: 3 でタスクを追加 → タイトルがグレーで表示される
  3. POST /tasks{"title": "test", "priority": 5} を送る → 400 が返る
  4. Swagger UI で tasks.json を確認して priority が保存されていること

★ 余裕がある人向け: 拡張課題

このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。

  • Vue 側でタスク一覧を priority 昇順で並び替えて表示する (computedsort())
  • 優先度ごとの件数 ("高: 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 も同様に priorityt.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 の bodypriority: 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/ フォルダは触らない (採点ロジック)
  • テンプレートの 表示テキスト (「カウント:」等) はテストが見ているので残す

このガイドのドリル一覧

#ドリル名タイトルタイプ難易度
101-fill-patch-deletefetch PATCH/DELETE - CRUD 4動詞の連携🔧 穴埋め
202-add-filtercomputed + filter - リスト絞り込み➕ 機能追加
303-add-priorityselect + 動的バインド - 優先度フィールド追加➕ 機能追加
404-build-fullstack-todoonMounted + 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()
}

確認方法

  1. POST /tasks でタスクを2件追加する
  2. チェックボックスをクリックすると done が反転して tasks.json に反映される
  3. 「削除」ボタンをクリックするとリストと 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 がすべて動く状態。

以下の機能を追加せよ。

追加する機能

  1. フィルタボタン: 「すべて」「未完了のみ」「完了のみ」の3つのボタンを追加する
  2. 絞り込み表示: ボタンの状態に応じて表示するタスクを絞り込む
  3. 件数表示: 「表示中 X 件 / 全 Y 件」を画面下部に表示する

ヒント

  • computed を使って tasks から絞り込んだ配列を派生させる
  • フィルタ状態は ref('all') のような文字列で管理する('all' / 'active' / 'done'
  • v-fortasks の代わりに computed の配列を使う

確認方法

  1. タスクを追加して一部を完了にする
  2. 「未完了のみ」で未完了タスクだけ表示される
  3. 「完了のみ」で完了タスクだけ表示される
  4. 件数が正しく変わる

★ 余裕がある人向け: 拡張課題

このドリルが 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 を送る

確認方法

  1. priority: 1 でタスクを追加 → タイトルが赤で表示される
  2. priority: 3 でタスクを追加 → タイトルがグレーで表示される
  3. POST /tasks{"title": "test", "priority": 5} を送る → 400 が返る
  4. Swagger UI で tasks.json を確認して priority が保存されていること

★ 余裕がある人向け: 拡張課題

このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。

  • Vue 側でタスク一覧を priority 昇順で並び替えて表示する (computedsort())
  • 優先度ごとの件数 ("高: 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 も同様に priorityt.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 の bodypriority: 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'
  • ___2loadTasks
  • ___3id
  • ___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.jsonpriority なし)との互換性はどうなる?
  • 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

フルスタック実装の思考プロセス

  1. データの流れを設計する — UI ↔ Vue state ↔ HTTP API ↔ FastAPI
  2. マウント時の初期化onMounted で API 呼び出し
  3. ユーザー操作 → API 呼び出し → state 更新 — 一連の流れを関数化
  4. エラーハンドリング — 本ドリルでは省略可、実務では try/catch で UI に伝える

「楽観的UI」 vs 「サーバー再取得」

このドリルでは サーバー再取得 (loadTasks()) 方式を採用:

パターン説明利点
サーバー再取得操作後に GET をもう一度叩いて全件取得シンプル、データのズレが出にくい
楽観的UIAPI レスポンスを待たず 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()
}

確認方法

  1. POST /tasks でタスクを2件追加する
  2. チェックボックスをクリックすると done が反転して tasks.json に反映される
  3. 「削除」ボタンをクリックするとリストと 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'
  • ___2loadTasks
  • ___3id
  • ___4'DELETE'

演習: 絞り込みフィルタを追加する

ベースコード(base/App.vue)は GET/POST/PATCH/DELETE がすべて動く状態。

以下の機能を追加せよ。

追加する機能

  1. フィルタボタン: 「すべて」「未完了のみ」「完了のみ」の3つのボタンを追加する
  2. 絞り込み表示: ボタンの状態に応じて表示するタスクを絞り込む
  3. 件数表示: 「表示中 X 件 / 全 Y 件」を画面下部に表示する

ヒント

  • computed を使って tasks から絞り込んだ配列を派生させる
  • フィルタ状態は ref('all') のような文字列で管理する('all' / 'active' / 'done'
  • v-fortasks の代わりに computed の配列を使う

確認方法

  1. タスクを追加して一部を完了にする
  2. 「未完了のみ」で未完了タスクだけ表示される
  3. 「完了のみ」で完了タスクだけ表示される
  4. 件数が正しく変わる

★ 余裕がある人向け: 拡張課題

このドリルが 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 を送る

確認方法

  1. priority: 1 でタスクを追加 → タイトルが赤で表示される
  2. priority: 3 でタスクを追加 → タイトルがグレーで表示される
  3. POST /tasks{"title": "test", "priority": 5} を送る → 400 が返る
  4. Swagger UI で tasks.json を確認して priority が保存されていること

★ 余裕がある人向け: 拡張課題

このドリルが PASS したら、以下の発展課題に挑戦してみてください (採点はなし、自由演習)。

  • Vue 側でタスク一覧を priority 昇順で並び替えて表示する (computedsort())
  • 優先度ごとの件数 ("高: 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 も同様に priorityt.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 の bodypriority: 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.jsonpriority なし)との互換性はどうなる?
  • 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()
}

解説

フルスタック実装の思考プロセス

  1. データの流れを設計する — UI ↔ Vue state ↔ HTTP API ↔ FastAPI
  2. マウント時の初期化onMounted で API 呼び出し
  3. ユーザー操作 → API 呼び出し → state 更新 — 一連の流れを関数化
  4. エラーハンドリング — 本ドリルでは省略可、実務では try/catch で UI に伝える

「楽観的UI」 vs 「サーバー再取得」

このドリルでは サーバー再取得 (loadTasks()) 方式を採用:

パターン説明利点
サーバー再取得操作後に GET をもう一度叩いて全件取得シンプル、データのズレが出にくい
楽観的UIAPI レスポンスを待たず state を先に更新UX が速い、ネットワーク遅延でも快適

スタートは「サーバー再取得」、慣れたら「楽観的UI」に書き換えてみると深い学習に。

実装が複数あり得る点

  • 状態管理: ref / reactive / Pinia (このドリルでは Pinia は不要)
  • fetch のラッパー: 関数毎に直接書く / 共通の api.js を作る
  • 完了状態の見せ方: 取り消し線 / 文字色変更 / アイコン
  • 削除確認ダイアログ: あり / なし

演習: テキスト検索フィルタを追加する

タスク一覧の上にテキスト入力を置き、 入力した文字列を タイトルに含むタスクだけ表示 するように実装せよ。

やること

  1. searchQuery という ref を追加 (v-model でテキスト入力にバインド)
  2. filteredTasks という computed を追加: searchQuerytitle に含むタスクのみ返す
  3. 大文字小文字を区別しない (Buy で "buy milk" もヒット)
  4. searchQuery が空なら全件表示
  5. テンプレート側で tasks.value の代わりに filteredTasks を表示

確認方法

  1. ブラウザで http://localhost:5173
  2. 「買い物」「Buy milk」「掃除」 のタスクを登録
  3. 検索欄に「buy」と入力 → "Buy milk" のみ表示される
  4. 検索欄を空にする → 全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">

tasksfilteredTasks に差し替えるだけ。

解説

なぜ computed か

computed依存する ref が変わると自動再計算 + 結果をキャッシュ

通常の関数だと renderingごとに毎回 filter が走ってしまうが、 computed なら taskssearchQuery が変わったときだけ計算する。

フロント側 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 フィールドがある)

やること

  1. sortBy という ref を追加 ('id' / 'priority')
  2. <select v-model="sortBy"> で切替UI を追加
  3. sortedTasks という computed を追加: sortBy に応じて並び替え
  4. priority は 数値昇順 (1 = 高優先 = 上)
  5. id は 数値昇順 (登録順)

確認方法

  1. priority=3 のタスクを先に登録、 priority=1 を後に登録
  2. プルダウンで「id 順」 → 登録順 (3→1)
  3. プルダウンで「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」 を押した時のみ実際に削除するようにせよ。

やること

  1. deleteTask 関数の冒頭で window.confirm("削除しますか?") を呼ぶ
  2. confirm が false を返したら早期 return (= 削除しない)
  3. confirm が true なら従来通り fetch DELETE

確認方法

  1. タスクを 3件登録
  2. 削除ボタンを押す → ダイアログ「削除しますか?」 が出る
  3. 「キャンセル」 → 削除されない
  4. 「OK」 → 削除される

仕様

  • window.confirm で十分 (= デフォルトの OK/キャンセル ボタン)
  • カスタムモーダルは不要 (★★★ で別 drill)

ヒント

async function deleteTask(id) {
  if (!window.confirm("削除しますか?")) return
  // 以下、 既存の fetch DELETE
}

window.confirmOKtrueキャンセルfalse

解説

なぜ確認ダイアログが必要か

DELETE は不可逆操作。 誤クリックで貴重なデータを失う事故を防ぐ。

window.confirm の制約

  • カスタマイズ不可 (OK / Cancel ボタンのみ、 多言語切替不可)
  • 同期的 (= ブロッキング) なのでスタイリッシュではない

実務では

  • カスタムモーダル (Headless UI / Vuetify / 自前) を使うのが一般的
  • 削除取り消し (Undo) パターンも検討 → 「削除しました [取消]」 トースト
  • ダブルクリック必須にする UI もある

本 drill は最小コストで「削除 = 確認」 のパターンを学ぶ目的。

演習: タスク名クリックで inline 編集

タスク名をクリックすると その場で input に切り替わり、 編集できるようにせよ。 Enter または blur で保存、 Escape でキャンセル。

やること

  1. editingId という ref を追加 (現在編集中の task id、 デフォルト null)
  2. タスク名 span をクリックすると editingId = task.id にして input 表示
  3. input の値が変わったら PATCH で保存
  4. Enter キーで保存 + 終了
  5. Escape キーで保存せずに終了 (元の値に戻す)
  6. blur (フォーカスが外れる) でも保存 + 終了

仕様

  • v-if / v-else でテキスト表示と input 表示を切替
  • 編集中の task は 1つだけ (editingId が単数)
  • 編集中に新規 task 追加・他 task 削除しても editing 状態は維持

確認方法

  1. タスク 3件登録
  2. 真ん中のタスク名をクリック → input に変わる
  3. 「修正後」 と入力 → Enter で確定
  4. ページリロード後も内容が保存されている (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 実行中に 「読み込み中…」 を表示し、 完了したら通常の一覧表示に戻すようにせよ。

やること

  1. loading という ref (boolean) を追加
  2. loadTasks の冒頭で loading.value = true、 最後 (finally) で false
  3. テンプレートで v-if="loading" で「読み込み中…」 を表示
  4. addTask / toggleDone / deleteTask 等の他 fetch でも loading を立てる

仕様

  • loading 中は input / button を disabled にする (連打防止)
  • 通常の一覧は v-if="!loading" で隠す or そのまま表示するかは任意
  • エラー時 (fetch reject) も finally で loading を解除

確認方法

  1. ブラウザの DevTools > Network > Throttling を "Slow 3G" に
  2. リロード → 「読み込み中…」 が一瞬表示される
  3. 追加ボタンを押す → 短時間 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 (= ぼやけたプレースホルダ) → 体感速度を改善する