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 / 自動採番 |
| 3 | 1 件ずつ開く・直す・捨てる | パスパラ / 404 / PATCH / DELETE |
| 4 | PC 再起動でもメモが残るようにする | JSON 永続化 |
| 5 | メモを tag や本文で探せるようにする | クエリパラメータ |
詰まったら starter / answer を使う
各 STEP には 2 種類の参考コードが用意されている:
| 種類 | いつ使う |
|---|---|
stepN/starter/main.py | STEP N に入る直前の状態。途中で追いつけなくなったらこれをコピーして再スタート |
stepN/answer/main.py | STEP N を完了した状態。自分のコードがおかしくなったらここを見て答え合わせ |
📌 step2/starter = step1/answer と同じ内容。
STEP を 1 つ進めるごとに「前のSTEPの完成形」=「次のSTEPのスタート地点」になる。
→ 完走できれば理想だが、詰まった時間で全体を諦めないこと。
starter を使って次のSTEPに進めば、機能の積み上げは体験できる。
最終ゴール: Swagger UI に並ぶ 5 つのエンドポイント
最終的にブラウザの /docs で以下の 5 つが並ぶ:
GET 一覧 / POST 作成 / GET 1件 / PATCH 更新 / DELETE 削除 = CRUD すべて。
最終ゴール: メモを取り出してみる
GET /memos を Try it out → Execute すると、保存されたメモが JSON で返る:
サーバーを再起動してもデータが残り、メモを書く・見る・直す・捨てる・探す が全て 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 API | JSON (データ) |
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
| 部分 | 意味 |
|---|---|
uvicorn | ASGI サーバー (実際にリクエストを受け付ける人) |
main | main.py のこと |
app | main.py の中の app = FastAPI() のこと |
--reload | ファイルを保存したら自動で再起動する |
起動成功:
INFO: Uvicorn running on http://127.0.0.1:8000
停止: Ctrl + C
ブラウザで確認
| URL | 何が見える |
|---|---|
| http://localhost:8000/memos | [] (空の配列) |
| http://localhost:8000/docs | Swagger UI (操作画面) |
Swagger UI の使い方:
- エンドポイント
GET /memosをクリック (展開される) - Try it out を押す (入力が有効化)
- Execute を押す (実際にリクエストが飛ぶ)
- Response body に結果が表示される
演習 1-1
main.py を作って、以下の状態まで持っていく:
- [ ]
uvicorn main:app --reloadで起動する - [ ] http://localhost:8000/memos が
[]を返す - [ ] Swagger UI (
/docs) でGET /memosを Try it out できる
詰まったら:
ModuleNotFoundError: fastapi→pip install fastapi uvicorn(環境構築で済んでいるはず)[Errno 48] Address already in use→ 古いサーバーが残っている。前のターミナルでCtrl+C
STEP1 まとめ
Memo アプリの「📖 メモ帳を開く」機能ができた (中身は空っぽだが)。
| 学んだ FastAPI の道具 | コード |
|---|---|
| FastAPI インスタンス | app = FastAPI() |
| GET エンドポイント | @app.get("/memos") |
| return で JSON | return [] |
| 起動 | uvicorn main:app --reload |
| 確認 | localhost:8000/docs |
STEP1 完成時の Swagger UI
http://localhost:8000/docs を開くとこの画面になっていれば OK。
→ 次は ✏️ メモを書き留める 機能を作る
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": "作成された"}| 違い | GET | POST |
|---|---|---|
| デコレータ | @app.get() | @app.post() |
| 用途 | 取得 | 作成 |
| status_code | 200 (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 で採番。
まだ done と created_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 memodefault=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 で:
POST /memosをクリック → Try it out- Request body に JSON を入力:
{"title": "買い物", "body": "牛乳", "tag": "private"}
- Execute → Response body に
{"id":1, ...}が返る 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: falseとcreated_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 が追加された。クリックして展開すると入力欄が自動で出る。
下に 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/1 | memo_id = 1 (int) で関数が呼ばれる |
GET /memos/abc | 422 (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 で「明示的に送られたキー」だけ取り出す。送られなかった title を None で潰す事故を防ぐ。
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 だけ書き換わる。
| PATCH | PUT |
|---|---|
| 部分更新 (送ったキーだけ) | 全体置き換え |
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=204 | No Content (削除成功の慣習) |
global memos | 関数内で memos = ... で再代入するので必要 |
return None | body を返さない |
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 で複数キー (title と done 同時) を送ってみる
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) が並ぶ。
→ ただし サーバを止めるとメモが全部消える。これでは実用にならない。
次は 💾 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()ヘルパーを書いた - [ ]
_findをmemosを引数で受け取る形に変えた - [ ] 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 に永続化される。
サーバを止めても次の起動で 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 を追加
title か body に文字列が含まれているメモを返す:
@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=work | tag フィルタ |
/memos?q=会議 | 全文検索 |
/memos?tag=work&q=会議 | 両方同時に効く (AND) |
Swagger UI で確認
GET /memos を開くと、tag と q の 入力欄が自動で出る:
- Pydantic モデルと同じく、関数引数から Swagger UI が自動生成される
- API の使い方を伝えるドキュメントを別途書かなくていい
- これが FastAPI の 「ただ書くだけで動く」 の正体
演習 5-1
- [ ]
GET /memosにtagクエリパラメータを追加 - [ ]
?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 を展開すると、tag と q のクエリパラメータ入力欄が自動で出る。
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 UI | 1 |
| ✏️ 書く | @app.post + Pydantic モデル + 422 自動 | 2 |
| 📂 1件開く | パスパラメータ + 型ヒント | 3 |
| ❓ 無いと伝える | HTTPException(404) | 3 |
| ✓ 直す | @app.patch + model_dump(exclude_unset=True) | 3 |
| 🗑 捨てる | @app.delete + status_code=204 | 3 |
| 💾 残る | JSON 永続化 (load/save パターン) | 4 |
| 🔍 探す | クエリパラメータ | 5 |