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

FastAPI で Memo アプリを作る

テクノロジ系 / L17 app-memo — Memo アプリ製作

FastAPI で Memo アプリを作る

自分専用のメモ帳サーバーを作る

作るもの: Memo アプリ

「思いついたことを書き留めて、後から見返せる」自分専用のメモ帳。

データは自分のPCに保存され、サーバーを再起動しても消えない。

ユーザー (=自分)              Memo アプリ (これから作るもの)
                          ┌─────────────────────┐
   ✏️ 書く        ──→     │  POST   /memos      │
   📖 一覧を見る    ──→    │  GET    /memos      │
   📂 1件開く      ──→    │  GET    /memos/{id} │
   ✓ 直す        ──→     │  PATCH  /memos/{id} │
   🗑 捨てる       ──→    │  DELETE /memos/{id} │
   🔍 探す        ──→     │  GET    /memos?q=…  │
                          └─────────────────────┘
                          データは memos.json に保存

Memo アプリの 6 つの機能

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

機能ユーザーから見るとSTEP
📖 一覧を見るメモ帳を開く1
✏️ 書き留める新しいメモを書く2
📂 1件開く・直す・捨てる既存のメモを操作する3
💾 残るPC を再起動してもメモが残っている4
🔍 探す溜まったメモから絞り込む5
➕ チャレンジより便利にする (任意)終盤

進め方

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

STEP作る機能裏で学ぶ FastAPI の道具
1メモ帳サーバーを起動して、まだ空っぽの一覧を返すFastAPI 起動 / GET / Swagger UI
2メモを書き留められるようにするPOST / Pydantic / 自動採番
31 件ずつ開く・直す・捨てるパスパラ / 404 / PATCH / DELETE
4PC 再起動でもメモが残るようにするJSON 永続化
5メモを tag や本文で探せるようにするクエリパラメータ

詰まったら 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 /memos を Try it out → Execute すると、保存されたメモが JSON で返る:

!w:1100

サーバーを再起動してもデータが残り、メモを書く・見る・直す・捨てる・探す が全て 1 つの画面から操作できる。

STEP 1

📖 メモ帳サーバーを起動する

まだメモはゼロ。でも「一覧を見にきた人」を受け入れる窓口だけ用意する

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

🚀サーバーが立つ = アプリの土台ができる
📖「メモを見にきた人」を受け入れる入り口が用意される
🛠Swagger UI が自動で出る → 動作確認に curl すらいらない
🧪中身が空でも「サーバーは生きてる」と確認できる

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

このSTEPで作るもの

Memo アプリの「メモ帳を開く」機能の最小形

  • メモ帳サーバーが起動する
  • 一覧を見にいくと「まだメモは無い (空っぽ)」と返す
  • ブラウザの Swagger UI から操作できる
ユーザー               Memo アプリ
  📖 一覧を見る  ──→   GET /memos  ──→  [] (空っぽ)

「URL を打ったら JSON が返る」を一番小さい形で体験する。

Web API とは

> URL を打ったらデータが返ってくる仕組み

あなた             FastAPI サーバー
                  ┌────────────┐
ブラウザを開く →  │  GET /     │ →  {"message": "Hello"}
                  └────────────┘
返すもの
Web サイトHTML (画面)
Web APIJSON (データ)

JSON は {"key": "value"} の形。Python の dict がそのまま JSON になる。

FastAPI が何をしてくれるか

> 「URL と Python 関数を結びつける」のが FastAPI の仕事

GET /          →  def hello():
GET /memos     →  def list_memos():       FastAPI が振り分ける
POST /memos    →  def create_memo():

おまけで自動でやってくれること:

  • Swagger UI (操作画面) の自動生成
  • 型チェック
  • エラー整形

最小コード: main.py を書く

from fastapi import FastAPI                  # ① ライブラリ取り込み

app = FastAPI(title="Memo API")              # ② API の本体を作る

@app.get("/memos")                           # ③ URL と HTTP メソッドを指定
def list_memos():                            # ④ 関数定義
    return []                                # ⑤ return すると JSON で返る

5 行。空のリスト [] を返すだけ。

title="Memo API" は Swagger UI に表示されるアプリ名。後で見て分かりやすくなる。

1行ずつの意味

from fastapi import FastAPI

FastAPI ライブラリから FastAPI クラスを持ってくる

app = FastAPI(title="Memo API")

API の「本体」インスタンスを作る。変数名は app で固定 (起動コマンド uvicorn main:app と一致させる)。title は Swagger UI のヘッダーに出るアプリ名

@app.get("/memos")

デコレータ。「GET /memos が来たら直下の関数を呼ぶ」という宣言

def list_memos(): return []

返した値が JSON になる。[] は JSON 配列 [] になる

サーバーを起動する

uvicorn main:app --reload
部分意味
uvicornASGI サーバー (実際にリクエストを受け付ける人)
mainmain.py のこと
appmain.py の中の app = FastAPI() のこと
--reloadファイルを保存したら自動で再起動する

起動成功:

INFO:     Uvicorn running on http://127.0.0.1:8000

停止: Ctrl + C

ブラウザで確認

URL何が見える
http://localhost:8000/memos[] (空の配列)
http://localhost:8000/docsSwagger UI (操作画面)

Swagger UI の使い方:

  1. エンドポイント GET /memos をクリック (展開される)
  2. Try it out を押す (入力が有効化)
  3. Execute を押す (実際にリクエストが飛ぶ)
  4. Response body に結果が表示される

演習 1-1

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

  • [ ] uvicorn main:app --reload で起動する
  • [ ] http://localhost:8000/memos が [] を返す
  • [ ] Swagger UI (/docs) で GET /memos を Try it out できる

詰まったら:

  • ModuleNotFoundError: fastapipip install fastapi uvicorn (環境構築で済んでいるはず)
  • [Errno 48] Address already in use → 古いサーバーが残っている。前のターミナルで Ctrl+C

STEP1 まとめ

Memo アプリの「📖 メモ帳を開く」機能ができた (中身は空っぽだが)。

学んだ FastAPI の道具コード
FastAPI インスタンスapp = FastAPI()
GET エンドポイント@app.get("/memos")
return で JSONreturn []
起動uvicorn main:app --reload
確認localhost:8000/docs

STEP1 完成時の Swagger UI

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

!w:1100

→ 次は ✏️ メモを書き留める 機能を作る

STEP 2

✏️ メモを書き留める

「思いついたことを送ったら、サーバーが覚えてくれる」

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

💡思いついた瞬間に書き留められる
🤖id・作成日時はサーバー側が勝手に付ける = ユーザーは title だけ書けば良い
🛡title を忘れたら「title が無い」と即 422 で教えてくれる (型チェック自動)
📋Swagger UI に入力欄が自動で出る = 試し書きが超ラク

ユーザーは 書きたいこと だけ考えれば良い。サーバーが面倒なことを引き受ける。

このSTEPで作るもの

Memo アプリの「✏️ メモを書く」機能

ユーザー                Memo アプリ
  ✏️ "買い物" を書く  ──→  POST /memos       ──→  覚える
  📖 一覧を見る       ──→  GET /memos        ──→  ["買い物"]

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

id・作成日時・完了フラグ はサーバー側が勝手に決める。

$ curl -X POST http://localhost:8000/memos \
    -d '{"title": "買い物", "body": "牛乳", "tag": "private"}'
# {"id":1, "title":"買い物", "body":"牛乳", "tag":"private",
#  "done":false, "created_at":"2026-06-05T10:45:00"}

POST の最小形

@app.post("/memos", status_code=201)
def create_memo():
    return {"message": "作成された"}
違いGETPOST
デコレータ@app.get()@app.post()
用途取得作成
status_code200 (default)201 (Created)

POST は 新規作成を意味するメソッド。慣習として status_code=201 を返す。

ボディを受け取るには Pydantic

from pydantic import BaseModel

class MemoCreate(BaseModel):       # ← リクエストボディの型
    title: str                     # 必須
    body: str = ""                 # デフォルトあり = 省略可
    tag: str = "default"

@app.post("/memos", status_code=201)
def create_memo(payload: MemoCreate):    # ← この型ヒントで自動受け取り
    return {"title": payload.title}

自動でやってくれること:

  • リクエスト JSON を MemoCreate に変換
  • title が無ければ 422 エラー を自動で返す
  • Swagger UI に入力欄が自動で出る

id の採番

「POST が来るたびに id を 1, 2, 3... と振りたい」

最初の素直な方法: メモリ上の list を持ち、max + 1 で採番

まだ donecreated_at は付けず、次のスライドで追加する。

memos: list[dict] = []     # サーバ起動中だけ存在する記憶領域

@app.post("/memos", status_code=201)
def create_memo(payload: MemoCreate):
    new_id = max([m["id"] for m in memos], default=0) + 1
    memo = {"id": new_id, "title": payload.title, "body": payload.body,
            "tag": payload.tag}
    memos.append(memo)
    return memo

default=0 がポイント。list が空の時 max([]) はエラーになるが、default=0 で 0 を返す。最初の id は 0+1 = 1

done と created_at を自動付与する

from datetime import datetime

@app.post("/memos", status_code=201)
def create_memo(payload: MemoCreate):
    new_id = max([m["id"] for m in memos], default=0) + 1
    memo = {
        "id": new_id,
        "title": payload.title,
        "body": payload.body,
        "tag": payload.tag,
        "done": False,                              # ← 作った直後は未完了
        "created_at": datetime.now().isoformat(timespec="seconds"),
    }
    memos.append(memo)
    return memo

サーバ側で決める項目 はクライアントから受け取らない。これが API 設計の定石。

一覧取得も書き換え

@app.get("/memos")
def list_memos():
    return memos        # ← STEP1 では [] を返していた箇所

これで以下のループが成立する:

POST /memos {"title":"..."} → memos に追加
GET  /memos               → memos を返す

Swagger UI で動かす

/docs で:

  1. POST /memos をクリック → Try it out
  2. Request body に JSON を入力:
   {"title": "買い物", "body": "牛乳", "tag": "private"}
   
  1. Execute → Response body に {"id":1, ...} が返る
  2. GET /memos → 今作ったメモが入っている

Swagger UI は実機テストの一番速い方法。curl は本格的に API を叩くようになってから覚えればよい。

型違反 (422 を観察する)

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

{"title": 123}        // ← title は str のはずなのに数値

→ 422 Unprocessable Entity:

{
  "detail": [{
    "loc": ["body", "title"],
    "msg": "Input should be a valid string",
    "type": "string_type"
  }]
}

Pydantic が自動で型チェックしてくれている。これを 自分で書かなくていい のが FastAPI の価値。

演習 2-1

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

  • [ ] MemoCreate モデルを定義した
  • [ ] POST /memos でメモを作れる (id=1, 2, 3...と連番になる)
  • [ ] done: falsecreated_at がサーバ側で自動付与される
  • [ ] GET /memos で全件返る
  • [ ] Swagger UI で title を抜くと 422 が返ることを確認した

時間が余ったら: tag を省略したときに "default" が入ることを確認する

STEP2 まとめ

Memo アプリの「✏️ 書く」と「📖 一覧で見る」が繋がった。

書いたものが一覧に出る、メモ帳として最低限の往復が成立。

学んだ FastAPI の道具コード
Pydantic モデルclass MemoCreate(BaseModel):
POST + 201 Created@app.post("/memos", status_code=201)
ボディの型受け取りdef create_memo(payload: MemoCreate):
id の自動採番max([m["id"] for m in memos], default=0) + 1
サーバ側で決める項目done=False, created_at=datetime.now()

STEP2 完成時の Swagger UI

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

!w:1100

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

→ 次は 📂 既存のメモを 1 件ずつ操作する 機能を作る

STEP 3

📂 メモを開く・直す・捨てる

「買い物メモを開いて、済マークを付けて、要らないメモは消す」

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

📂一覧から「1件だけ」見たい時、必要な情報だけ取れる
完了したタスクに済マークを付けられる (=メモ帳が ToDo リストにもなる)
✏️書き間違えても直せる、後で内容を加筆できる
🗑要らないメモを片付けられる = メモ帳が散らかったまま放置されない
存在しないメモを取りに行くと「無い」と教えてくれる (404)

メモ帳として 「書く・読む・直す・消す」 の基本動作 4 種 がここで揃う。

これでメモ帳としての最低限の道具立てが完成。

このSTEPで作るもの

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

ユーザー                       Memo アプリ
  📂 1番のメモを開く     ──→   GET    /memos/1   → 1件返す
  ❓ 999番を開こうとした  ──→   GET    /memos/999 → 「無いよ」(404)
  ✓ 1番を完了にする     ──→   PATCH  /memos/1   → done を更新
  🗑 1番を捨てる         ──→   DELETE /memos/1   → 消す

ここまで終われば、メモ帳としての C(書く)・R(読む)・U(直す)・D(消す) が全て揃う。

📂 1 件のメモを開く

「1番のメモを開きたい」 = URL に /memos/1 と打って、その1件を返す。

@app.get("/memos/{memo_id}")          # ← {memo_id} が変数
def get_memo(memo_id: int):           # ← 型ヒントで自動変換
    for m in memos:
        if m["id"] == memo_id:
            return m
    return {"error": "not found"}     # ← まだ仮実装
URL何が起こる
GET /memos/1memo_id = 1 (int) で関数が呼ばれる
GET /memos/abc422 (int に変換できない)

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

❓ 存在しないメモを開こうとされたら

ユーザーが GET /memos/999 を叩いたが、999番のメモは無い。

「無いよ」とちゃんと伝える必要がある。

FastAPI では 例外で投げる のが作法。

from fastapi import FastAPI, HTTPException

@app.get("/memos/{memo_id}")
def get_memo(memo_id: int):
    for m in memos:
        if m["id"] == memo_id:
            return m
    raise HTTPException(status_code=404, detail="memo not found")

raise した瞬間に FastAPI が:

  • ステータスコードを 404 にする
  • {"detail": "memo not found"} を JSON で返す

呼び出し側 (フロント等) はこの 404 を見て「無いんだな」と分かる。

探す処理を関数に切り出す

PATCH も DELETE も「memo_id から探す」処理が共通。先に ヘルパー関数 にしておく。

def _find(memo_id: int) -> dict:
    for m in memos:
        if m["id"] == memo_id:
            return m
    raise HTTPException(status_code=404, detail="memo not found")

これを GET / PATCH / DELETE 全部で使う:

@app.get("/memos/{memo_id}")
def get_memo(memo_id: int):
    return _find(memo_id)

3行で書けるようになった。重複を関数化 するのは Python の基本作法。

✓ メモを直す (済マークを付ける)

「1番のメモに済マーク (done=true) を付けたい」=done だけを書き換える。

title や body は触らない、というのを表現するのが PATCH。

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

@app.patch("/memos/{memo_id}")
def update_memo(memo_id: int, payload: MemoUpdate):
    memo = _find(memo_id)
    for key, value in payload.model_dump(exclude_unset=True).items():
        memo[key] = value          # 送られてきたキーだけ更新
    return memo

ポイント: exclude_unset=True で「明示的に送られたキー」だけ取り出す。送られなかった titleNone で潰す事故を防ぐ。

PATCH の動作例

memos に {"id":1, "title":"買い物", "done":false} がある状態で:

curl -X PATCH http://localhost:8000/memos/1 \
  -H "Content-Type: application/json" \
  -d '{"done": true}'

# {"id":1, "title":"買い物", "done":true, ...}

title は送っていないので元のまま。done だけ書き換わる。

PATCHPUT
部分更新 (送ったキーだけ)全体置き換え

Memo API は PATCH で実装する。

🗑 メモを捨てる

「もう要らないメモを消す」=DELETE。

削除に成功したら body 無し (204 No Content) を返すのが慣習。

@app.delete("/memos/{memo_id}", status_code=204)
def delete_memo(memo_id: int):
    global memos
    _find(memo_id)                 # 無ければ 404
    memos = [m for m in memos if m["id"] != memo_id]
    return None                    # 204 は body 無しの慣習
部分意味
status_code=204No Content (削除成功の慣習)
global memos関数内で memos = ... で再代入するので必要
return Nonebody を返さない

memos.remove(...) でもいいが、リスト内包表記の方が読みやすい。

演習 3-1

  • [ ] GET /memos/{memo_id} で 1件取れる / 存在しないと 404
  • [ ] _find ヘルパー関数を作った
  • [ ] MemoUpdate モデルを定義した (全部 Optional)
  • [ ] PATCH /memos/{memo_id}done だけ書き換えられる
  • [ ] DELETE /memos/{memo_id} で消える / 存在しないと 404
  • [ ] Swagger UI で全部のメソッドを試した

時間が余ったら: PATCH で複数キー (titledone 同時) を送ってみる

STEP3 まとめ

Memo アプリの「📂 開く・直す・捨てる」が揃った。書く・読む・直す・消す = メモ帳としての基本動作 5 種類が全て動く。

学んだ FastAPI の道具コード
パスパラメータ@app.get("/memos/{memo_id}") + memo_id: int
404 を返すraise HTTPException(status_code=404, detail="...")
共通処理を関数化def _find(memo_id) -> dict:
部分更新payload.model_dump(exclude_unset=True)
削除成功status_code=204 + return None

STEP3 完成時の Swagger UI

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

!w:1100

→ ただし サーバを止めるとメモが全部消える。これでは実用にならない。

次は 💾 PC を再起動してもメモが残る ようにする。

STEP 4

💾 PCを再起動してもメモが残る

実用に耐えるメモ帳にする最後のピース

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

💾PC を再起動してもメモが消えない (=本物のメモ帳になる)
🔌電源を落としても安心、再起動後も同じメモが読める
👀memos.json を直接開けば中身が人間にも読める (デバッグ可能)
必要なら memos.json を直接編集することもできる
📦メモを誰かに渡すには memos.json をコピーするだけ

STEP3 まではメモリ上だけ = アプリ終了でリセット。

これで 実用に耐えるメモ帳 になる。

このSTEPで作るもの

Memo アプリの「💾 残る」機能

STEP3 まで:

uvicorn main:app --reload    # 起動して 3 件メモを書く
Ctrl+C                       # サーバー停止
uvicorn main:app --reload    # 再起動
curl http://localhost:8000/memos  # → []  メモが消えた…

memosメモリ上の list。プロセスが死ぬと一緒に消える。

これじゃメモ帳として実用にならない

やりたいこと: メモを memos.json というファイルに保存して、起動時に読み直す。

json.dump で書き込み (使い方の確認)

まずは Python 単体で json を書き出す手触りを確認する (本実装ではこの後の load/save パターンを使う):

import json

memos = [{"id": 1, "title": "買い物"}]

with open("memos.json", "w", encoding="utf-8") as f:
    json.dump(memos, f, ensure_ascii=False, indent=2)
オプション意味
encoding="utf-8"文字化け防止
ensure_ascii=False日本語をそのまま出力 (デフォルトだと 買い物 のように Unicode エスケープ表記になる)
indent=2整形 (人が読めるように)

memos.json の中身:

[
  {"id": 1, "title": "買い物"}
]

json.load で読み込み

with open("memos.json", "r", encoding="utf-8") as f:
    memos = json.load(f)

これで Python の list が戻ってくる。

load / save パターン

毎リクエストごとに「ファイルから読む → 操作する → 書き戻す」を繰り返す。

from pathlib import Path

DB_FILE = Path("memos.json")

def load() -> list[dict]:
    if not DB_FILE.exists():
        return []
    return json.loads(DB_FILE.read_text(encoding="utf-8"))

def save(memos: list[dict]) -> None:
    DB_FILE.write_text(
        json.dumps(memos, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )

Path.read_text / write_text を使うと with open を書かなくていい。短い。

各エンドポイントを load/save で書き換え (GET / POST)

GET 一覧 — メモリの memos ではなく、毎回 load() で取る:

@app.get("/memos")
def list_memos():
    return load()

POST — load → 操作 → save の 3 ステップに変わる:

@app.post("/memos", status_code=201)
def create_memo(payload: MemoCreate):
    memos = load()                         # ← ロード
    memo = {"id": _next_id(memos), "title": payload.title, ...}
    memos.append(memo)
    save(memos)                            # ← セーブ
    return memo

各エンドポイントを load/save で書き換え (DELETE)

DELETE は global memos が不要になるのが嬉しい:

@app.delete("/memos/{memo_id}", status_code=204)
def delete_memo(memo_id: int):
    memos = load()                         # ← ローカル変数
    _find(memos, memo_id)
    memos = [m for m in memos if m["id"] != memo_id]
    save(memos)

PATCH も同じパターン (load → 部分更新 → save)。

load/save 化に伴う共通の変更

Before (STEP3)After (STEP4)
グローバル memos: list[dict] = []削除 (毎回 load() で取る)
def _find(memo_id)def _find(memos, memo_id) (引数で受け取る)
max([m["id"] for m in memos], default=0) + 1 を直書きdef _next_id(memos) に関数化
DELETE の global memos不要 (ローカル変数になるから)

「memos はファイルが本物の置き場、関数の中ではローカル変数として load/save する」 が今回の発想。

_next_id を関数化:

def _next_id(memos: list[dict]) -> int:
    return max((m["id"] for m in memos), default=0) + 1

永続化を体感する

# サーバ起動
uvicorn main:app --reload

# 3件作る
curl -X POST http://localhost:8000/memos -H "..." -d '{"title":"A"}'
curl -X POST http://localhost:8000/memos -H "..." -d '{"title":"B"}'
curl -X POST http://localhost:8000/memos -H "..." -d '{"title":"C"}'

# Ctrl+C → ターミナルで以下を確認
cat memos.json    # ← 3件入っている

# 再起動
uvicorn main:app --reload
curl http://localhost:8000/memos   # ← 3件残っている

これが永続化。プロセスが死んでもデータが残る。

永続化の罠

起こること
同時書き込み競合2人同時に PATCH → 片方が消える
大きなファイル起動時の load() が重くなる
Path("memos.json") の cwd 依存サーバを違うフォルダから起動すると別ファイルになる

本格運用ではデータベース (SQLite / PostgreSQL) に移る。この規模なら JSON で十分。

演習 4-1

  • [ ] load()save() ヘルパーを書いた
  • [ ] _findmemos を引数で受け取る形に変えた
  • [ ] POST / PATCH / DELETE で load → 操作 → save している
  • [ ] 3件作って、Ctrl+C → 再起動 → 残っていることを確認した
  • [ ] cat memos.json で中身が日本語のまま見えることを確認した

時間が余ったら: memos.json を手で編集してサーバ再起動 → 反映されることを確認

STEP4 まとめ

Memo アプリに「💾 残る」機能が付いた。

PC を再起動してもメモが残るようになり、メモ帳として実用できる状態になった。

学んだ FastAPI の道具コード
永続化ファイルDB_FILE = Path("memos.json")
ロードjson.loads(DB_FILE.read_text(...))
セーブDB_FILE.write_text(json.dumps(...))
パターンload → 操作 → save を全エンドポイントで
日本語対応ensure_ascii=False

STEP4 完成時の Swagger UI

エンドポイントは STEP3 と同じ。違いは「保存先」: 書いたメモが memos.json に永続化される。

!w:1100

サーバを止めても次の起動で memos.json から読み直されるので、メモは残る。

→ メモが溜まってくると一覧から目当てを探すのが大変になる。

次は 🔍 探す 機能を作る。

STEP 5

🔍 溜まったメモから探す

メモが100件溜まっても、目的のメモにたどり着けるようにする

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

🏷tag で「仕事だけ」「プライベートだけ」を切り出せる
🔍「あの会議のメモ、なんて書いたっけ?」を本文検索で見つけられる
🤝条件を組み合わせて絞り込める (例: 仕事タグ + 会議という言葉)
📋Swagger UI に絞り込み入力欄が自動で出る = 探すための画面を別途作らなくていい
📈メモが 10 件でも 1000 件でも「探せる」状態を保てる

メモは溜まっていくのが普通。溜まっても腐らせない ようにするピース。

このSTEPで作るもの

Memo アプリの「🔍 探す」機能

ユーザー                      Memo アプリ
  🔍 仕事タグだけ見たい  ──→   GET /memos?tag=work  →  workタグだけ
  🔍 "会議" を探したい   ──→   GET /memos?q=会議     →  本文に含む
  🔍 仕事の会議だけ      ──→   GET /memos?tag=work&q=会議
curl http://localhost:8000/memos?tag=work
# work タグだけ返ってくる

curl http://localhost:8000/memos?q=会議
# title か body に "会議" が入っているメモだけ返ってくる

クエリパラメータの基本

URL の ?key=value&key=value 部分。

@app.get("/memos")
def list_memos(tag: str | None = None):
    # tag に何が入っているか:
    # GET /memos          → None
    # GET /memos?tag=work → "work"
    memos = load()
    if tag is not None:
        memos = [m for m in memos if m["tag"] == tag]
    return memos

パスに {} で出てこない引数は自動でクエリパラメータ扱い

デフォルト値 = None を付けると省略可能になる。

全文検索 q を追加

titlebody に文字列が含まれているメモを返す:

@app.get("/memos")
def list_memos(tag: str | None = None, q: str | None = None):
    memos = load()
    if tag is not None:
        memos = [m for m in memos if m["tag"] == tag]
    if q is not None:
        memos = [m for m in memos if q in m["title"] or q in m["body"]]
    return memos
URL動作
/memos全件
/memos?tag=worktag フィルタ
/memos?q=会議全文検索
/memos?tag=work&q=会議両方同時に効く (AND)

Swagger UI で確認

GET /memos を開くと、tagq入力欄が自動で出る:

  • Pydantic モデルと同じく、関数引数から Swagger UI が自動生成される
  • API の使い方を伝えるドキュメントを別途書かなくていい
  • これが FastAPI の 「ただ書くだけで動く」 の正体

演習 5-1

  • [ ] GET /memostag クエリパラメータを追加
  • [ ] ?tag=work で work タグのメモだけ返る
  • [ ] ?q=会議 で title/body に「会議」が含まれるメモだけ返る
  • [ ] ?tag=work&q=会議 で AND 動作することを確認
  • [ ] Swagger UI で 2 つの入力欄が自動で出ることを確認

時間が余ったら: 完了済み (done=true) だけ返す ?done=true を追加

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

機能エンドポイント
📖 一覧を見るGET /memos
✏️ 書くPOST /memos body: {title, body?, tag?}
📂 1件開くGET /memos/{id}
✓ 直すPATCH /memos/{id} body: {title?, body?, tag?, done?}
🗑 捨てるDELETE /memos/{id}
💾 残るmemos.json に load/save
🔍 探すGET /memos?tag=…&q=…

サーバーを再起動してもデータが残る、ユーザー操作 6 種類を備えたメモアプリ。

STEP5 完成時の Swagger UI

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

!w:1100

Swagger UI から直接「tag=work」「q=会議」を入れて Try it out できる。

チャレンジ (任意)

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

チャレンジ題 (より便利なメモ帳にする)

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

アイデアユーザーが嬉しいこと
🧹 完了済みを一括掃除終わったメモをまとめて消したいDELETE /memos?done=true
📅 新しい順に並べたい直近書いたメモを上に?sort=created_at
📄 100件あっても10件ずつ一覧が長すぎないように?limit=10&offset=20
📥 まとめて取り込みたい配列で一気に追加POST /memos/bulk
🏷 タグごとに何件か知りたいtag 集計GET /memos/tags
📤 メモを外に持ち出したいCSV エクスポートGET /memos/export

エンドポイント名・パラメータ名は自由に決めてOK。動けば正解

FAQ

質問回答
memos.json はどこに作られる?uvicorn を実行したディレクトリ
--reload した瞬間 memos.json が消える?消えない。再 import されるだけ
連番 ID が削除後に詰まらない?詰まらない (1,2,3 → 削除 → 1,3 → 次は 4)
PATCH と PUT どっち使う?部分更新したいなら PATCH
422 が出るリクエスト JSON の型違反。Swagger UI で再現させて確認
Swagger UI で日本語が文字化けensure_ascii=False の付け忘れ

振り返り

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

Memo アプリの機能使った FastAPI の道具STEP
📖 一覧を見るFastAPI インスタンス + @app.get + Swagger UI1
✏️ 書く@app.post + Pydantic モデル + 422 自動2
📂 1件開くパスパラメータ + 型ヒント3
❓ 無いと伝えるHTTPException(404)3
✓ 直す@app.patch + model_dump(exclude_unset=True)3
🗑 捨てる@app.delete + status_code=2043
💾 残るJSON 永続化 (load/save パターン)4
🔍 探すクエリパラメータ5

おつかれさまでした

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