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

FastAPI で Task 管理アプリを作る

テクノロジ系 / L18 app-task-sqlite — Task 管理アプリ (SQLite)

FastAPI で Task 管理アプリを作る

データの置き場を「ファイル」から「データベース」に変える

作るもの: Task 管理アプリ

「やることを登録して、終わったらチェックを付ける」自分専用の ToDo サーバー。

データは SQLite データベース に保存する。

ユーザー (=自分)                Task アプリ (これから作るもの)
                           ┌──────────────────────────┐
   📋 一覧を見る     ──→    │  GET    /tasks            │
   ✏️ 登録する      ──→    │  POST   /tasks            │
   📂 1件開く       ──→    │  GET    /tasks/{id}       │
   ✓ 完了にする     ──→    │  PATCH  /tasks/{id}       │
   🗑 消す          ──→     │  DELETE /tasks/{id}       │
   🔍 完了/未完で絞る ──→   │  GET    /tasks?done=…     │
                           └──────────────────────────┘
                           データは tasks.db (SQLite) に保存

この教材のテーマ: なぜ「ファイル」をやめるのか

前回は、データを JSON ファイル に保存した。それで動く。

でも、人数や件数が増えると、ファイル保存には限界が出てくる。

ファイル保存 (JSON) の困りごと何が起きるか
🔁 毎回ファイル全体を読み書き件数が増えると重い
👥 2人が同時に書き込む片方の変更が消える
🔍 「完了したものだけ」を探す全件ループして自分で絞り込む必要がある
🔢 id の採番を自分で管理max + 1 のような処理を毎回書く

→ これらを データベース (DB) が肩代わりしてくれる。

ここでは SQLite という最小の DB を使う。

SQLite を選ぶ理由

説明
📦 インストール不要Python に最初から入っている (import sqlite3)。環境構築なし
📄 ファイル 1 個tasks.db というファイル 1 つが DB 本体。持ち運べる
🗄 本物の DBSELECT / INSERT / UPDATE / DELETE という SQL で操作する
🚀 学びが移植できるここで覚えた SQL は PostgreSQL / MySQL でもほぼ同じ

> この教材は SQL の文法を体系的に学ぶものではない

> 「FastAPI から DB を使うと、こう書ける」を体験する。SQL 文は資料に全部載っている。

Task アプリの 5 つの機能

これから 5 つの STEP で、順に機能を足していく。

機能ユーザーから見るとSTEP
📋 一覧を見るやることリストを開く1
✏️ 登録する新しいタスクを足す2
📂 1 件開く・消す特定のタスクを見る / 消す3
✓ 完了にする・直すチェックを付ける / 名前を直す4
🔍 完了/未完で絞る終わったものだけ表示する5

進め方

1 つ前の STEP で書いたコードに、次の STEP で機能を足していく。最後には 1 つの動く Task アプリが手元に残る。

STEP作る機能裏で学ぶ FastAPI + SQL の道具
1サーバー起動 + DB とテーブルを用意sqlite3 接続 / CREATE TABLE / SELECT
2タスクを登録するPOST / Pydantic / INSERT / DB 採番
31 件開く・消すパスパラ / SELECT WHERE / 404 / DELETE
4完了にする・直すPATCH / UPDATE / 部分更新
5完了/未完で絞るクエリパラメータ / WHERE done=?

詰まったら starter / answer を使う

各 STEP には 2 種類の参考コードが用意されている:

種類いつ使う
stepN/starter/main.pySTEP N に入る直前の状態。途中で追いつけなくなったらこれをコピーして再スタート
stepN/answer/main.pySTEP N を完了した状態。自分のコードがおかしくなったらここを見て答え合わせ

📌 step2/starter = step1/answer と同じ内容。

STEP を 1 つ進めるごとに「前のSTEPの完成形」=「次のSTEPのスタート地点」になる。

→ 完走できれば理想だが、詰まった時間で全体を諦めないこと。

starter を使って次のSTEPに進めば、機能の積み上げは体験できる。

最終ゴール: Swagger UI に並ぶ 5 つのエンドポイント

最終的にブラウザの /docs で以下の 5 つが並ぶ:

!w:1100

GET 一覧 / POST 作成 / GET 1件 / PATCH 更新 / DELETE 削除 = CRUD すべて。

最終ゴール: 完了タスクだけ取り出してみる

GET /tasks?done=true を Try it out → Execute すると、完了済みのタスクだけが JSON で返る:

!w:1100

「完了したものだけ」の絞り込みを、WHERE done = 1 という SQL 1 行で DB に任せられる。

STEP 1

📋 タスク一覧サーバーを起動する

サーバーを立てて、データの置き場 (DB とテーブル) を用意する

この機能があると何が嬉しい?

🚀サーバーが立つ = アプリの土台ができる
🗄DB とテーブルが用意される = データの「置き場所」が決まる
🛠Swagger UI が自動で出る → 動作確認に curl すらいらない
🧪中身が空でも「サーバーと DB は生きてる」と確認できる

土台ができたら、後は「機能を足していく」だけになる。

このSTEPで作るもの

Task アプリの「📋 一覧を開く」機能の最小形

  • サーバーが起動する
  • 起動時に SQLite に tasks テーブルを用意する
  • 一覧を見にいくと「まだ何も無い (空っぽ)」と返す
ユーザー              Task アプリ            SQLite (tasks.db)
  📋 一覧を見る ──→  GET /tasks  ──→  SELECT * FROM tasks  ──→  [] (空)

「DB に問い合わせて、結果を JSON で返す」を一番小さい形で体験する。

DB に接続する関数を作る

import sqlite3

DB_FILE = "tasks.db"

def get_conn() -> sqlite3.Connection:
    conn = sqlite3.connect(DB_FILE)
    conn.row_factory = sqlite3.Row   # 行を dict のように扱えるようにする
    return conn
部分意味
sqlite3.connect("tasks.db")tasks.db ファイルに繋ぐ (無ければ自動で作られる)
conn.row_factory = sqlite3.Row取り出した行を row["title"] のように名前で読める形にする

sqlite3Python に最初から入っているpip install は不要。

テーブルを用意する

DB の中に「タスクを入れる箱 = テーブル」を作る。これは起動時に 1 回だけ。

def init_db() -> None:
    conn = get_conn()
    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS tasks (
            id         INTEGER PRIMARY KEY AUTOINCREMENT,
            title      TEXT    NOT NULL,
            done       INTEGER NOT NULL DEFAULT 0,
            created_at TEXT    NOT NULL
        )
        """
    )
    conn.commit()
    conn.close()

init_db()   # ← サーバ起動時に呼ぶ

CREATE TABLE の中身を読む

CREATE TABLE IF NOT EXISTS tasks (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    title      TEXT    NOT NULL,
    done       INTEGER NOT NULL DEFAULT 0,
    created_at TEXT    NOT NULL
)
カラム意味
IF NOT EXISTSすでにあれば作り直さない (2回目以降の起動でも安全)
id ... AUTOINCREMENTDB が自動で 1, 2, 3... と採番する (自分で max+1 しなくていい)
title TEXT NOT NULL文字列。空 (NULL) は許さない
done INTEGER ... DEFAULT 0完了フラグ。SQLite に真偽値型は無いので 0/1 の数値 で表す。初期値 0 (未完了)
created_at TEXT NOT NULL作成日時を文字列で持つ

一覧を返す: SELECT

from fastapi import FastAPI

app = FastAPI(title="Task API")

@app.get("/tasks")
def list_tasks():
    conn = get_conn()
    rows = conn.execute("SELECT * FROM tasks").fetchall()
    conn.close()
    return [dict(row) for row in rows]
部分意味
SELECT * FROM taskstasks テーブルの全行を取り出す SQL
.fetchall()結果を全部リストで受け取る
dict(row)1 行を {"id":1, "title":"...", ...} の辞書に変換

辞書のリストを return すると、そのまま JSON 配列になる。

サーバーを起動して確認

uvicorn main:app --reload
URL何が見える
http://localhost:8000/tasks[] (まだ空。でも DB とテーブルはできている)
http://localhost:8000/docsSwagger UI (操作画面)

ターミナルで確認:

ls            # tasks.db ができている

tasks.db が出現していれば、DB の用意は成功。

演習 1-1

main.py を作って、以下の状態まで持っていく:

  • [ ] get_conn()init_db() を書いた
  • [ ] uvicorn main:app --reload で起動する
  • [ ] tasks.db がフォルダにできている
  • [ ] http://localhost:8000/tasks が [] を返す
  • [ ] Swagger UI (/docs) で GET /tasks を Try it out できる

詰まったら:

  • ModuleNotFoundError: fastapipip install fastapi uvicorn (sqlite3 は不要、標準装備)
  • [Errno 48] Address already in use → 古いサーバーが残っている。Ctrl+C

STEP1 まとめ

Task アプリの「📋 一覧を開く」機能と、DB の土台ができた。

学んだ道具コード
DB に接続sqlite3.connect("tasks.db")
名前で読める行conn.row_factory = sqlite3.Row
テーブル作成CREATE TABLE IF NOT EXISTS tasks (...)
全件取得SELECT * FROM tasks
行→辞書dict(row)

STEP1 完成時の Swagger UI

http://localhost:8000/docs を開くとこの画面になっていれば OK。

!w:1100

→ 次は ✏️ タスクを登録する 機能を作る

STEP 2

✏️ タスクを登録する

「やることを送ったら、DB が 1 行として覚えてくれる」

この機能があると何が嬉しい?

✏️やることをその場で登録できる
🔢id は DB が自動で振る = max+1 を自分で書かなくていい
🤖done (未完了) と作成日時はサーバー側が勝手に決める
🛡title を忘れたら「title が無い」と即 422 で教えてくれる
📋Swagger UI に入力欄が自動で出る = 試し書きが超ラク

ユーザーは やること だけ書けば良い。id 採番という面倒を DB が引き受ける。

このSTEPで作るもの

Task アプリの「✏️ 登録する」機能

ユーザー              Task アプリ            SQLite
  ✏️ "牛乳を買う" ──→  POST /tasks ──→  INSERT INTO tasks ... ──→  1行追加
  📋 一覧を見る   ──→  GET /tasks  ──→  SELECT * FROM tasks ──→  ["牛乳を買う"]

ユーザーが送るのは title だけ。

id・done・created_at はサーバーと DB が決める。

POST /tasks  {"title": "牛乳を買う"}
→ {"id": 1, "title": "牛乳を買う", "done": 0,
   "created_at": "2026-06-08T09:00:00"}

ボディを受け取る Pydantic モデル

from pydantic import BaseModel

class TaskCreate(BaseModel):
    title: str       # 必須。これだけ受け取る

前回と同じ。ユーザーから受け取るのは title だけ。

id・done・created_at は 受け取らず、サーバ側で決める (API 設計の定石)。

INSERT 文で 1 行追加する

from datetime import datetime

@app.post("/tasks", status_code=201)
def create_task(payload: TaskCreate):
    created_at = datetime.now().isoformat(timespec="seconds")
    conn = get_conn()
    cur = conn.execute(
        "INSERT INTO tasks (title, created_at) VALUES (?, ?)",
        (payload.title, created_at),
    )
    conn.commit()
    new_id = cur.lastrowid                  # ← DB が振った id
    row = conn.execute(
        "SELECT * FROM tasks WHERE id = ?", (new_id,)
    ).fetchone()
    conn.close()
    return dict(row)

INSERT 文の 4 つのポイント

conn.execute(
    "INSERT INTO tasks (title, created_at) VALUES (?, ?)",
    (payload.title, created_at),
)
ポイント説明
(title, created_at) だけ指定id は AUTOINCREMENT、done は DEFAULT 0 なので書かなくていい
VALUES (?, ?)?プレースホルダ。値を直接埋め込まず ? で受ける
(payload.title, created_at)? に入る値をタプルで渡す
conn.commit()これを忘れると保存されない。変更を確定する合図

> なぜ ? を使う? → 文字列を直接 SQL に埋めると危険 (SQL インジェクション)。

> ? で渡すと SQLite が安全に処理してくれる。必ず ? を使う

登録直後のデータを返す

cur = conn.execute("INSERT ...", (...))
conn.commit()
new_id = cur.lastrowid                      # 今 INSERT した行の id
row = conn.execute("SELECT * FROM tasks WHERE id = ?", (new_id,)).fetchone()
return dict(row)
部分意味
cur.lastrowid直前に INSERT した行の id を DB が教えてくれる
.fetchone()1 行だけ取り出す (一覧の fetchall と対)

登録したタスクを「id・done・created_at 付き」で返すと、ユーザーが結果を確認できる。

型違反 (422 を観察する)

POST /tasks のボディを わざと壊して 送ってみる:

{}                  // ← title が無い

→ 422 Unprocessable Entity:

{
  "detail": [{
    "loc": ["body", "title"],
    "msg": "Field required",
    "type": "missing"
  }]
}

Pydantic が「title が必須なのに無い」と自動で弾く。自分でチェックを書かなくていい

演習 2-1

main.py を以下まで持っていく:

  • [ ] TaskCreate モデルを定義した (title: str)
  • [ ] POST /tasks でタスクを登録できる (id=1, 2, 3... と DB が採番)
  • [ ] done=0created_at がサーバ側で自動付与される
  • [ ] GET /tasks で登録したタスクが返る
  • [ ] Swagger UI で title を抜くと 422 が返ることを確認した

時間が余ったら: 2件登録して、id が自動で 1→2 になることを確認する

STEP2 まとめ

Task アプリの「✏️ 登録」と「📋 一覧」が DB 経由で繋がった。

学んだ道具コード
Pydantic モデルclass TaskCreate(BaseModel):
行を追加INSERT INTO tasks (...) VALUES (?, ?)
安全な値渡しプレースホルダ ?
変更を確定conn.commit()
DB が振った idcur.lastrowid

STEP2 完成時の Swagger UI

POST /tasks が追加された。クリックして展開すると入力欄が自動で出る。

!w:1100

下に TaskCreate という Schemas が出ているのも自動生成 = Pydantic で型定義した恩恵。

→ 次は 📂 1 件開く・消す 機能を作る

STEP 3

📂 1 件開く・消す

「特定のタスクだけ開いて、要らなくなったら消す」

この機能があると何が嬉しい?

📂一覧から「1件だけ」を取り出せる
🗑終わって不要になったタスクを消せる
存在しないタスクを取りに行くと「無い」と教えてくれる (404)
🎯WHERE id = ? で DB が目的の 1 行を探してくれる (自分でループしない)

このSTEPで作るもの

Task アプリの「📂 1件ずつ操作する」2 機能

ユーザー                       Task アプリ            SQLite
  📂 1番を開く     ──→  GET    /tasks/1   ──→  SELECT ... WHERE id=1
  ❓ 999番を開く   ──→  GET    /tasks/999 ──→  該当なし → 404
  🗑 1番を消す     ──→  DELETE /tasks/1   ──→  DELETE ... WHERE id=1

パスパラメータ {task_id} で「何番のタスク?」を受け取り、SQL の WHERE id = ? に渡す。

パスパラメータ + SELECT WHERE

@app.get("/tasks/{task_id}")          # ← {task_id} が変数
def get_task(task_id: int):           # ← 型ヒントで自動変換
    conn = get_conn()
    row = conn.execute(
        "SELECT * FROM tasks WHERE id = ?", (task_id,)
    ).fetchone()
    conn.close()
    return dict(row)                  # ← まだ仮実装 (404 はこの後)
URL何が起こる
GET /tasks/1task_id = 1WHERE id = 1 の行を取得
GET /tasks/abc422 (int に変換できない)

{task_id}波括弧の中の名前関数引数の名前 を一致させる。

404 を正しく返す: 探すヘルパー

「無いタスクを取りに行ったら 404 で教える」。GET も DELETE も共通なので ヘルパー関数にする。

from fastapi import FastAPI, HTTPException

def _find(conn: sqlite3.Connection, task_id: int) -> sqlite3.Row:
    row = conn.execute(
        "SELECT * FROM tasks WHERE id = ?", (task_id,)
    ).fetchone()
    if row is None:                    # ← .fetchone() は無ければ None を返す
        conn.close()
        raise HTTPException(status_code=404, detail="task not found")
    return row

fetchone()None を返したら「その id は無い」。そこで 404 を投げる。

GET 1件を仕上げる

@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    conn = get_conn()
    row = _find(conn, task_id)        # 無ければここで 404
    conn.close()
    return dict(row)

3行で書けるようになった。重複処理を関数化 するのは前回 Memo でもやった作法。

🗑 タスクを消す: DELETE 文

@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
    conn = get_conn()
    _find(conn, task_id)              # 無ければ 404
    conn.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
    conn.commit()                    # ← 削除も commit が必要
    conn.close()
    return None                      # 204 は body 無しの慣習
部分意味
DELETE FROM tasks WHERE id = ?指定 id の行を消す SQL
conn.commit()INSERT と同じく、変更を確定する
status_code=204No Content (削除成功の慣習)

JSON ファイル時代の「リスト内包表記で作り直し」は不要。SQL 1 行で消える。

演習 3-1

  • [ ] _find ヘルパー関数を作った (無ければ 404)
  • [ ] GET /tasks/{task_id} で 1件取れる / 存在しないと 404
  • [ ] DELETE /tasks/{task_id} で消える / 存在しないと 404
  • [ ] Swagger UI で全部のメソッドを試した

時間が余ったら:

  • /tasks/abc (文字列) を GET → 422 になることを確認
  • 消した後にもう一度 GET /tasks/1 → 404 になることを確認

STEP3 まとめ

Task アプリの「📂 開く」と「🗑 消す」が揃った。

学んだ道具コード
パスパラメータ@app.get("/tasks/{task_id}") + task_id: int
1行を探すSELECT * FROM tasks WHERE id = ? + .fetchone()
無い判定if row is None:
404 を返すraise HTTPException(status_code=404, ...)
1行を消すDELETE FROM tasks WHERE id = ?

STEP3 完成時の Swagger UI

GET一覧 / POST / GET 1件 / DELETE が並ぶ。

!w:1100

→ 次は ✓ 完了にする・直す 機能を作る

STEP 4

✓ 完了にする・直す

「終わったタスクにチェックを付ける / タイトルを直す」

この機能があると何が嬉しい?

終わったタスクに「完了」を付けられる = ToDo として機能する
✏️タイトルのタイポを後から直せる
🎯「送ったキーだけ更新」= 触っていない項目を誤って消さない
🗄UPDATE ... WHERE id=? で DB が該当行だけ書き換える

「完了にできる」ことで、ただの一覧が やることリスト になる。

このSTEPで作るもの

Task アプリの「✓ 完了にする・直す」機能

ユーザー                    Task アプリ            SQLite
  ✓ 1番を完了に  ──→  PATCH /tasks/1  ──→  UPDATE tasks SET done=1 WHERE id=1
  ✏️ 1番の名前を直す ─→  PATCH /tasks/1  ──→  UPDATE tasks SET title=? WHERE id=1

PATCH部分更新。送ったキーだけ書き換える。

title を送らなければ title はそのまま。done を送らなければ done はそのまま。

部分更新用の Pydantic モデル

class TaskUpdate(BaseModel):
    title: str | None = None      # 全部 Optional
    done: bool | None = None

前回 Memo の MemoUpdate と同じパターン。全フィールドを省略可能にする。

done は受け取りは bool (true/false)、DB へは 0/1 で保存する。

UPDATE 文で「送られたキーだけ」書き換える

@app.patch("/tasks/{task_id}")
def update_task(task_id: int, payload: TaskUpdate):
    conn = get_conn()
    _find(conn, task_id)                              # 無ければ 404
    fields = payload.model_dump(exclude_unset=True)   # 送られたキーだけ
    if "title" in fields:
        conn.execute("UPDATE tasks SET title = ? WHERE id = ?",
                     (fields["title"], task_id))
    if "done" in fields:
        conn.execute("UPDATE tasks SET done = ? WHERE id = ?",
                     (int(fields["done"]), task_id))   # True→1 / False→0
    conn.commit()
    row = conn.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)).fetchone()
    conn.close()
    return dict(row)

このコードの 3 つのポイント

ポイント説明
exclude_unset=True送られたキーだけ 取り出す (前回 Memo と同じ)。送っていない項目は触らない
if "title" in fields:送られたものだけ UPDATE する。if で分けるとカラム名を安全に固定できる
int(fields["done"])True/False1/0 に変換。SQLite の done は数値カラム

> なぜ if で 1 つずつ分ける? → SQL のカラム名を変数で組み立てると危険。

> 「title なら title の UPDATE」と固定で書くのが安全。

PATCH の動作例

{"id":1, "title":"牛乳を買う", "done":0} がある状態で:

PATCH /tasks/1   {"done": true}
→ {"id":1, "title":"牛乳を買う", "done":1, ...}

title は送っていないので元のまま。done だけ 0→1 になる。

PATCH /tasks/1   {"title": "低脂肪牛乳を買う"}
→ {"id":1, "title":"低脂肪牛乳を買う", "done":1, ...}

done は送っていないので元のまま (1 を維持)。

演習 4-1

  • [ ] TaskUpdate モデルを定義した (title, done どちらも Optional)
  • [ ] PATCH /tasks/{task_id}done だけ送って、title が変わらないことを確認
  • [ ] done: true を送ると DB に 1 が入ることを確認
  • [ ] PATCH /tasks/999 → 404 が返ることを確認
  • [ ] Swagger UI で動作確認した

時間が余ったら: titledone を同時に送って、両方変わることを確認

STEP4 まとめ

Task アプリに「✓ 完了にする・直す」が加わり、CRUD が揃った。

学んだ道具コード
部分更新モデルclass TaskUpdate(BaseModel): (全部 Optional)
送られたキーだけpayload.model_dump(exclude_unset=True)
行を書き換えUPDATE tasks SET title = ? WHERE id = ?
bool→数値int(fields["done"])

STEP4 完成時の Swagger UI

CRUD 5 メソッド (GET一覧 / POST / GET 1件 / PATCH / DELETE) が並ぶ。

!w:1100

→ 次は 🔍 完了/未完で絞る 機能を作る

STEP 5

🔍 完了/未完で絞る

「終わったタスクだけ」「まだのタスクだけ」を切り替えて見る

この機能があると何が嬉しい?

🔍?done=true で完了したものだけ表示できる
📋?done=false でまだ終わっていないものだけ表示できる
絞り込みを DB に任せられる (WHERE done = ? の 1 行)
🆚JSON ファイルなら全件ループして自分で絞った。SQL なら DB がやる

これが「ファイルをやめて DB にした」一番分かりやすい恩恵。

このSTEPで作るもの

Task アプリの「🔍 絞り込む」機能

ユーザー                      Task アプリ            SQLite
  🔍 完了だけ見たい ──→  GET /tasks?done=true  ──→  WHERE done = 1
  🔍 未完だけ見たい ──→  GET /tasks?done=false ──→  WHERE done = 0
  📋 全部見たい     ──→  GET /tasks            ──→  WHERE なし (全件)

クエリパラメータで SQL を切り替える

@app.get("/tasks")
def list_tasks(done: bool | None = None):
    conn = get_conn()
    if done is None:                              # 指定なし → 全件
        rows = conn.execute("SELECT * FROM tasks").fetchall()
    else:                                          # 指定あり → 絞り込み
        rows = conn.execute(
            "SELECT * FROM tasks WHERE done = ?", (int(done),)
        ).fetchall()
    conn.close()
    return [dict(row) for row in rows]
URLdone の値実行される SQL
/tasksNoneSELECT * FROM tasks
/tasks?done=trueTrue → 1... WHERE done = 1
/tasks?done=falseFalse → 0... WHERE done = 0

クエリパラメータの基本 (復習)

def list_tasks(done: bool | None = None):
  • パスに {} で出てこない引数は自動でクエリパラメータ扱い (前回 Memo と同じ)
  • = None を付けると省略可能になる
  • 型を bool にすると ?done=true / ?done=false を自動で True/False に変換

bool → DB の数値カラムに渡すときは int(done) で 1/0 に直す (PATCH と同じ考え方)。

JSON ファイル時代との比較

JSON ファイル (前回)SQLite (今回)
絞り込み[t for t in tasks if t["done"]]自分で書くWHERE done = ?DB に任せる
件数が多い時全件メモリに読んでループDB が効率よく探す
id 採番max + 1 を自分でAUTOINCREMENT で DB が
データの安全同時書き込みで壊れることもDB がある程度守ってくれる

> 小さいアプリなら JSON で十分。だが「増えても困らない」のは DB の強み。

演習 5-1

  • [ ] GET /tasksdone クエリパラメータを追加 (done: bool | None = None)
  • [ ] ?done=true で完了タスクだけ返る
  • [ ] ?done=false で未完了タスクだけ返る
  • [ ] /tasks (指定なし) で全件返る
  • [ ] Swagger UI で done の入力欄が自動で出ることを確認

時間が余ったら: タスクを数件作り、いくつか完了にして、3 パターン (全件 / 完了 / 未完) を試す

完成形チェック: Task アプリで何ができるか

機能エンドポイント使う SQL
📋 一覧を見るGET /tasksSELECT *
✏️ 登録するPOST /tasks body: {title}INSERT
📂 1件開くGET /tasks/{id}SELECT ... WHERE id=?
✓ 完了/直すPATCH /tasks/{id} body: {title?, done?}UPDATE
🗑 消すDELETE /tasks/{id}DELETE
🔍 絞り込むGET /tasks?done=…WHERE done=?

データは tasks.db に保存され、CRUD すべてが SQL で動く Task アプリ。

STEP5 完成時の Swagger UI

GET /tasks を展開すると、done のクエリパラメータ入力欄が自動で出る。

!w:1100

Swagger UI から直接「done=true」を入れて Try it out できる。

チャレンジ (任意)

余裕があれば取り組む追加題

チャレンジ題 (より便利な Task アプリにする)

好きなだけ手を伸ばす。「自分が欲しい使い心地」を実装する場。

アイデアユーザーが嬉しいことヒント
🔍 タイトルで検索「牛乳ってタスクどこ?」WHERE title LIKE ? (%牛乳%)
📅 新しい順に並べる直近作ったものを上にORDER BY created_at DESC
🧹 完了済みを一括削除終わったものをまとめて消すDELETE FROM tasks WHERE done = 1
📊 件数を数える残りいくつ?SELECT COUNT(*) FROM tasks WHERE done = 0
📝 期限を足すいつまでにやる?テーブルに due_date カラムを追加

SQL 文・パラメータ名は自由に決めてOK。動けば正解

FAQ

質問回答
tasks.db はどこにできる?uvicorn を実行したディレクトリ
tasks.db を消すとどうなる?データが全部消える。次の起動で空のテーブルが再作成される
done が 0/1 で返るのが気になるSQLite に真偽値型が無いため。0=未完 / 1=完了
commit() を忘れると?保存されない。INSERT/UPDATE/DELETE の後は必ず commit
なぜ ? を使う?値を直接 SQL に埋めると SQL インジェクションの危険。? で安全に渡す
422 が出るリクエストの型違反。Swagger UI で再現させて確認

振り返り

「Task アプリのこの機能を作るために、この FastAPI + SQL の道具を使った」を一覧で:

Task アプリの機能使った道具STEP
📋 一覧を見る@app.get + SELECT * + テーブル作成1
✏️ 登録する@app.post + Pydantic + INSERT2
📂 1件開くパスパラメータ + SELECT WHERE3
❓ 無いと伝えるfetchone() が None → HTTPException(404)3
🗑 消す@app.delete + DELETE WHERE3
✓ 完了/直す@app.patch + UPDATE + exclude_unset4
🔍 絞り込むクエリパラメータ + WHERE done=?5

前回 (JSON ファイル) からの進化

前回 Memo (JSON)今回 Task (SQLite)
データの置き場memos.jsontasks.db (DB)
保存json.dump でファイル全体を書くINSERT/UPDATE/DELETE で 1 行ずつ
id 採番max + 1 を自分でAUTOINCREMENT で DB が
絞り込みリスト内包表記で自分でWHERE で DB が

API の形 (GET/POST/PATCH/DELETE) は同じ。中身の「保存方法」が進化した。

これが「FastAPI + DB」の実務の入り口。

おつかれさまでした

力試ししたい方は クイズ形式の演習(任意) もあります。読むだけでも十分です。