PythonのWebフレームワークFastAPIを使って、ブラウザから操作できるTODOアプリを1ファイルで作ってみました。データベースもテンプレートエンジンも使わず、タスクは辞書に保存するインメモリ構成です。追加・完了・削除の3操作はフォームとリダイレクトだけで実現し、リロードしても二重送信されないPRGパターンも取り入れました。
FastAPIの基本だけでWebアプリがちゃんと動くところまでを、コードを断片ごとに分解しながら初心者向けに解説します。
FastAPIの基本概念、要件定義、実装、動作確認までを順番に学べます。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。
動画の内容をテキストで確認する
オープニング。FastAPIでWebアプリを作るカリキュラムを始めます。概要紹介。
FastAPIの基本をTODOアプリ開発で学ぶ 一覧・追加・完了切り替え・削除を1ファイルで実装する コードを段階的に組み立て、節目ごとに動作を確認する 最後にブラウザで完成したアプリを操作する 具体的にやること。
ルートURLでTODO一覧と未完了件数を表示する フォームへ入力したTODOを追加するTODOの完了・未完了を切り替える 指定したTODOを削除する 各POST処理後は303リダイレクトで一覧へ戻る 実装環境・必須アプリ。
OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、Webブラウザ、ターミナル パッケージ:fastapi、uvicorn、python-multipart FastAPIとは何か。
FastAPIはPythonでAPIやWebアプリのバックエンドを構築するためのWebフレームワークです。型ヒントを利用した入力検証と自動ドキュメントを備え、関数単位でルートを定義できます。
ASGIに対応し、非同期処理を扱えるPythonの型ヒントから入力を検証できるOpenAPIに基づくAPIドキュメントを自動生成できる デコレーターでURLと処理関数を結び付ける FastAPIでアプリを開発する重要要素。WebアプリではURLとHTTPメソッド、入力値、レスポンス、状態管理を分けて考えることが重要です。
今回のTODOアプリで各要素を最小構成から確認します。GETは画面表示、POSTはデータ変更に使い分けるFormでHTMLフォームの入力値を受け取るHTMLResponseとRedirectResponseを用途別に返す 入力値はstripとhtml.escapeで整える FastAPIのルーティングと状態管理。
ルーティングはブラウザの操作をPython関数へ届ける仕組みです。TODOの状態はIDをキーにした辞書へ保存し、追加・更新・削除を各ルートから行います。
URLパスのtodo_idで操作対象を特定する 共通の追加関数でID発行と保存をまとめる 辞書のgetとpopの既定値で存在しないIDを安全に扱うPOST後にGETへ戻してフォーム再送信を防ぐ アプリの要件定義。
起動直後に初期TODOが3件表示される 未完了件数が2件と表示される 新しいTODOを追加すると一覧と件数へ反映される 追加したTODOを完了にすると取り消し線が付き件数が減る 削除操作後に対象TODOが一覧から消える PREVIEW: 入力したコードをブラウザで確認。初期タスク3件と、未完了2件が表示されることを確認します。
FORM: 新しいTODOを入力。フォーム値はtitleとして受け取られ、strip()後に辞書へ保存されます。ADD OK: TODO追加と303遷移に成功。
追加した行が一覧へ反映され、未完了件数が2件から3件へ増えました。COMPLETE: 完了状態の切り替えに成功。追加したTODOへ取り消し線が付き、未完了件数も2件へ戻りました。
学習内容のまとめ。FastAPIではURLとHTTPメソッドを処理関数へ結び付けるGETは表示、POSTは追加・更新・削除に使い分けるTODOは辞書へ保存し、IDで対象を特定する 入力値を整形・エスケープし、安全にHTMLへ表示する 小さく実行確認しながら、完成挙動まで段階的に組み立てる エンディング。研修ならCodeCampで検索。
目次
- PythonのWebフレームワーク「FastAPI」とは
- PythonのFastAPIで作るTODOアプリの要件定義
- Python FastAPIでアプリを開発する際の重要な要素
- PythonのFastAPIにおけるルーティングと状態管理
- Python・FastAPIで開発する場合の環境構築(Windows 11/PowerShellで検証)
- PythonのFastAPIでTODOアプリを段階的に実装する流れ
- PythonのFastAPIで作るTODOアプリの完成コード
- PythonのFastAPIで作ったTODOアプリの動作確認
- PythonのFastAPIによるTODOアプリ開発で起きやすいエラーと対処法
- PythonのFastAPIで作るミニWebアプリの活用例
- PythonのFastAPIによるTODOアプリ開発のまとめ
- 参考にした一次情報
PythonのWebフレームワーク「FastAPI」とは
FastAPIはPythonでAPIやWebアプリのバックエンドを構築するためのWebフレームワークです。型ヒントを利用した入力検証と自動ドキュメントを備え、関数単位でルートを定義できます。
まず全体像を理解し、その後で今回のTODOアプリにどう関係するかを確認します。
- ASGIに対応し、非同期処理を扱える
- Pythonの型ヒントから入力を検証できる
- OpenAPIに基づくAPIドキュメントを自動生成できる
- デコレーターでURLと処理関数を結び付ける
FastAPIが担当する役割
ブラウザから届いたHTTPリクエストを受け取り、URLとメソッドに対応するPython関数を呼び出し、その戻り値をHTTPレスポンスとして返します。
画面の見た目を作るHTMLやCSSとは役割が異なり、FastAPIは主にリクエストを処理するバックエンドを担当します。
- リクエストを受け取る
- 対応する処理関数を選ぶ
- 処理結果をレスポンスとして返す
型ヒントと入力検証
FastAPIは関数の引数に付けた型ヒントを読み取り、入力値の変換や検証に利用します。型が合わない場合は、処理関数へ渡す前にエラーとして扱えます。
今回のアプリではフォーム値を文字列として受け取り、TODO IDは整数として扱うことで、どの値を期待しているかをコード上で明確にします。
- 引数の型がAPIの入力仕様になる
- 不正な値を処理関数へ渡しにくくなる
- エディターの補完や静的解析にも役立つ
自動ドキュメントとASGI
定義したルートと型情報からOpenAPI形式の仕様が作られ、/docsで操作可能なAPIドキュメントを確認できます。
FastAPIはASGIに対応しており、UvicornなどのASGIサーバー上で動作します。
-
/docsでルートと入力仕様を確認できる - UvicornがHTTP通信を受け持つ
- 必要に応じて非同期処理へ拡張できる
PythonのFastAPIで作るTODOアプリの要件定義
目的は、FastAPIのルーティングとフォーム処理を使い、ブラウザで操作できるTODOアプリを1ファイルで完成させることです。
対象者はPythonの関数・辞書・条件分岐を学び、次にWebアプリ開発を体験したい初学者で、完成物はTODOの一覧表示、追加、完了切り替え、削除ができるインメモリWebアプリです。
実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。
機能要件
- ルートURLでTODO一覧と未完了件数を表示する
- フォームへ入力したTODOを追加する
- TODOの完了・未完了を切り替える
- 指定したTODOを削除する
- 各POST処理後は303リダイレクトで一覧へ戻る
非機能要件
- 利用者が入力したタイトルをHTMLエスケープして表示する
- 存在しないTODO IDを指定してもKeyErrorで停止しない
- 学習用として外部データベースを使わず1ファイルで動かす
- サーバー再起動時にデータが消える制約を明示する
実装方針
今回はFastAPIの基本動作を追いやすくするため、アプリ本体、HTML生成、ルーティングを1つのPythonファイルへまとめます。
TODOはIDをキーにした辞書へ保存します。外部データベースを使わないため、サーバーを再起動すると追加したデータが消える学習用の構成です。
- 一覧表示はGET、追加・完了切り替え・削除はPOSTで処理する
- 処理後は303リダイレクトで一覧へ戻し、フォームの再送信を防ぐ
- HTML生成は専用関数へまとめ、ルート処理を短く保つ
- 入力値は空白を除去し、HTMLエスケープしてから表示する
完成と判断する条件
- 起動直後に初期TODOが3件表示される
- 未完了件数が2件と表示される
- 新しいTODOを追加すると一覧と件数へ反映される
- 追加したTODOを完了にすると取り消し線が付き件数が減る
- 削除操作後に対象TODOが一覧から消える
Python FastAPIでアプリを開発する際の重要な要素
WebアプリではURLとHTTPメソッド、入力値、レスポンス、状態管理を分けて考えることが重要です。今回のTODOアプリで各要素を最小構成から確認します。
まず全体像を理解し、その後で今回のTODOアプリにどう関係するかを確認します。
- GETは画面表示、POSTはデータ変更に使い分ける
- FormでHTMLフォームの入力値を受け取る
- HTMLResponseとRedirectResponseを用途別に返す
- 入力値はstripとhtml.escapeで整える
- インメモリ保存は再起動で消えるため学習用途に限定する
URLとHTTPメソッドを分けて考える
URLは操作対象を表し、HTTPメソッドはその対象へ何をするかを表します。今回のアプリでは一覧表示をGET、状態を変更する操作をPOSTへ分けます。
表示と更新を分けると、ブラウザの再読み込みやフォーム再送信による意図しない二重登録を防ぎやすくなります。
- GET
/はTODO一覧を表示する - POST
/addは新しいTODOを追加する - POST
/toggle/{todo_id}と/delete/{todo_id}は指定IDを操作する
入力とレスポンスの役割
HTMLフォームの値はFormで受け取り、画面表示にはHTMLResponse、操作後の画面遷移にはRedirectResponseを使います。
受け取った文字列はstrip()で前後の空白を除去し、表示時にはhtml.escape()でHTMLとして解釈されないようにします。
- 入力値が空ならTODOを追加しない
- 画面表示とリダイレクトを用途別に使う
- 利用者の入力をそのままHTMLへ埋め込まない
状態をどこへ保存するか
Webアプリはリクエストをまたいでデータを保持する場所が必要です。今回は仕組みを追いやすくするため、Pythonの辞書を保存領域として使います。
辞書は高速で分かりやすい一方、プロセスを終了すると内容が消えます。永続化が必要なアプリではデータベースへ置き換えます。
- TODO IDを辞書のキーにする
- タイトルと完了状態を値として保持する
- 本番運用ではデータベースや認証も検討する
PythonのFastAPIにおけるルーティングと状態管理
ルーティングはブラウザの操作をPython関数へ届ける仕組みです。TODOの状態はIDをキーにした辞書へ保存し、追加・更新・削除を各ルートから行います。
まず全体像を理解し、その後で今回のTODOアプリにどう関係するかを確認します。
- URLパスのtodo_idで操作対象を特定する
- 共通の追加関数でID発行と保存をまとめる
- 辞書のgetとpopの既定値で存在しないIDを安全に扱う
- POST後にGETへ戻してフォーム再送信を防ぐ
デコレーターでルートを登録する
@app.get()や@app.post()を関数の直前へ書くと、URLとHTTPメソッドに対応する処理関数としてFastAPIへ登録されます。
/toggle/{todo_id}のような波括弧部分はパスパラメーターで、URLに含まれるIDを関数の引数として受け取れます。
- デコレーターの直下へ処理関数を書く
- 同じURLでもHTTPメソッドが違えば別処理にできる
- パスパラメーターで操作対象を指定する
辞書でTODOの状態を管理する
各TODOはID、タイトル、完了状態を持つ辞書として保存します。IDをキーにすると、一覧表示・更新・削除で同じTODOを特定できます。
存在しないIDが指定された場合に備え、get()や既定値付きのpop()を使って例外を避けます。
- 追加時に重複しないIDを発行する
- 完了切り替えでは
doneを反転する - 削除では対象IDの要素を辞書から取り除く
POST・Redirect・GETの流れ
フォーム操作をPOSTで受け付けた後、そのままHTMLを返すのではなく303リダイレクトで一覧へ戻します。ブラウザは続けてGETを行い、更新後の状態を表示します。
この流れはPRGパターンと呼ばれ、再読み込み時に同じPOSTが再送信される問題を防ぎます。
- POSTで状態を変更する
- 303レスポンスで一覧URLを返す
- GETで更新後の画面を表示する
Python・FastAPIで開発する場合の環境構築(Windows 11/PowerShellで検証)
この記事のセットアップ手順と掲載コードは、Windows 11 Pro、PowerShell 5.1、Python 3.13.3で動作確認しています。仮想環境を有効化せず、その中のPythonを直接指定するため、以下のコマンドはPowerShellとコマンドプロンプト(cmd)の両方で使えます。
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install fastapi uvicorn python-multipart
macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。
python3 -m venv .venv
./.venv/bin/python -m pip install fastapi uvicorn python-multipart
- FastAPIのFormでフォーム値を受け取るにはpython-multipartが必須。入れ忘れるとサーバ起動時にエラーになる
- 起動は .\.venv\Scripts\python.exe -m uvicorn todo_app:app --reloadで、ブラウザからhttp://127.0.0.1:8000を開く
- データはメモリ上の辞書に保存するため、サーバを再起動(--reloadの自動再起動を含む)するとタスクは初期状態に戻る
PythonのFastAPIでTODOアプリを段階的に実装する流れ
最初から全機能を一度に書くのではなく、役割ごとに小さく組み立てます。
各段階で確認する内容を決めておくと、問題が起きた場所を特定しやすくなります。
実装する順序
- 必要なモジュールを読み込み、FastAPIアプリとTODO保存用の辞書を用意する
- TODOを1件追加する共通関数と、初期表示用のデータを作る
- TODO一覧を安全なHTMLへ変換する関数を作る
- 一覧表示、追加、完了切り替え、削除のルートを定義する
- ブラウザから一連の操作を行い、表示と件数の変化を確認する
各段階で確認すること
次の確認を順番に行い、完成状態まで機能を積み上げます。
- 依存関係を読み込めるか確認:FastAPI、Form、2種類のレスポンス型を実際にimportします。
- 最小のTODOを1件保存:_add_todo()を呼び、連番IDと辞書への保存を確認します。
- 初期データの件数を確認:起動時に3件が登録され、そのうち未完了が2件であることを確認します。
- HTMLを最後まで生成できるか確認:_render_page()を実行し、HTML文字列とTODO行が生成されることを確認します。
- 一覧表示ルートを確認:FastAPIへGET /が登録され、HTML画面を返す準備ができたか確認します。
- 追加処理と303遷移を確認:add()を直接呼び、TODOが増えて303レスポンスになることを確認します。
- 完了状態の反転を確認:toggle()で未完了のタスクが完了へ切り替わることを確認します。
- 削除処理を確認:delete()で指定IDが辞書から消え、303で一覧へ戻ることを確認します。
- 完成した全ルートを確認:一覧・追加・完了切替・削除の4ルートが登録済みか最終確認します。
PythonのFastAPIで作るTODOアプリの完成コード
動画の内容をテキストで確認する
INTRO: FastAPI TODOアプリの実装を開始。コードの役割を1行ずつ確認し、節目ごとに実行しながら完成まで組み立てます。LINE 001: ファイルの目的を記述。
FastAPI製のインメモリTODOアプリであることをdocstringで示します。LINE 003: HTMLエスケープ機能を読み込む。入力文字列を安全にHTMLへ埋め込むため、標準ライブラリhtmlを使います。
LINE 004: 連番を作るcountを読み込む。TODOごとに重複しないIDを1から順番に発行します。LINE 006: FastAPI本体とFormを読み込む。
FastAPIはアプリ本体、FormはPOSTされたフォーム値の受け取りに使います。LINE 007: 2種類のレスポンスを読み込む。HTMLResponseは画面表示、RedirectResponseは操作後の遷移に使います。
RUN 1/9: 依存関係を読み込めるか確認。FastAPI、Form、2種類のレスポンス型を実際にimportします。CHECK 1/9: 途中実行に成功。
CHECK: FastAPIとレスポンス型の読み込みに成功 LINE 009: 次の処理区分を示すコメント。アプリ本体とインメモリの保存領域に関するコードをここからまとめます。LINE 010: FastAPIアプリを生成。
titleは自動生成されるAPIドキュメントにも表示されるアプリ名です。LINE 011: TODO保存用の辞書を用意。整数IDをキー、各TODOの辞書を値としてメモリ上だけに保持します。
LINE 012: IDを1から発行。next()を呼ぶたび1、2、3と進むカウンターです。LINE 015: _add_todo関数を定義。
引数と戻り値の型を明示し、この後のインデント内へ処理を書きます。LINE 016: 次のIDを取り出す。追加のたびにカウンターを進め、今回のTODO専用IDを取得します。
LINE 017: TODOを辞書へ保存。ID・タイトル・完了状態をまとめ、IDをキーに登録します。RUN 2/9: 最小のTODOを1件保存。
_add_todo()を呼び、連番IDと辞書への保存を確認します。CHECK 2/9: 途中実行に成功。CHECK: 最小TODO 1件 / ID 1 LINE 020: 次の処理区分を示すコメント。
動作イメージが分かる初期タスク(サーバを再起動すると消えるインメモリデータ)に関するコードをここからまとめます。LINE 021: 完了済みサンプルを登録。done=Trueで、取り消し線表示も最初から確認できるようにします。
LINE 022: 未完了サンプルを登録。doneを省略すると既定値Falseになり、未完了TODOとして追加されます。LINE 023: 削除確認用サンプルを登録。
削除ボタンの動作をすぐ試せる3件目のTODOです。RUN 3/9: 初期データの件数を確認。起動時に3件が登録され、そのうち未完了が2件であることを確認します。
CHECK 3/9: 途中実行に成功。CHECK: 初期TODO 3件 / 未完了2件 LINE 026: 次の処理区分を示すコメント。一覧画面のHTML組み立てに関するコードをここからまとめます。
LINE 027: _render_page関数を定義。引数と戻り値の型を明示し、この後のインデント内へ処理を書きます。LINE 028: TODO行の格納先を用意。
各TODOから作ったli要素を順番にためる空リストです。LINE 029: 全TODOを順番に処理。辞書の値を走査し、1件ずつ一覧表示用HTMLへ変換します。
LINE 030: タイトルをHTMLエスケープ。山括弧や引用符を無害化し、入力がHTMLとして実行されるのを防ぎます。LINE 031: 完了時だけCSSクラスを付ける。
doneならdoneクラス、未完了なら空文字を選びます。LINE 032: 状態に合わせてボタン名を変更。完了済みなら戻す、未完了なら完了と表示します。
LINE 033: HTML埋め込み用IDを取り出す。完了切替と削除のURLへ同じTODO IDを渡す準備です。LINE 034: 1件分のHTML追加を開始。
ここから作るli要素をrowsリストへ追加します。LINE 035: TODO行のli要素を開始。完了状態に応じたCSSクラスをliへ埋め込みます。
LINE 036: 安全化したタイトルを表示。escape済みのタイトルを表示領域へ差し込みます。LINE 037: 操作ボタン領域を開始。
完了切替と削除の2フォームをまとめます。LINE 038: 完了切替フォームを作る。TODO IDをURLへ含め、POSTで対象を特定します。
LINE 039: 完了・戻すボタンを配置。現在状態に合わせたラベルでフォームを送信します。LINE 040: 削除フォームを作る。
削除対象のTODO IDをURLパスへ埋め込みます。LINE 041: 削除ボタンを配置。dangerクラスで通常操作と色を分けます。
LINE 042: 1件分のHTMLを閉じる。操作領域とli要素を閉じてTODO行を完成させます。LINE 043: append呼び出しを閉じる。
複数行で組み立てた1件分のHTMLをrowsへ確定します。LINE 044: 未完了件数を集計。doneがFalseのTODOだけを1として合計します。
LINE 045: 一覧または空表示を選択。TODOがあればrowsを連結し、0件なら空状態メッセージを使います。LINE 046: HTML文書をf文字列で開始。
変数を埋め込める三重引用符の文字列として画面全体を返します。LINE 047: HTML文書と言語を指定。lang=jaで文書の主言語が日本語だと伝えます。
LINE 048: 画面設定のheadを開始。文字コード・表示幅・タイトル・CSSを中へ置きます。LINE 049: 文字コードをUTF-8に指定。
日本語を文字化けさせず表示する設定です。LINE 050: モバイル表示幅を指定。端末幅に合わせてレイアウトするviewport設定です。
LINE 051: ブラウザのタイトルを指定。タブなどに表示されるページ名です。LINE 052: ページ内CSSを開始。
1ファイルで完結させるため見た目もHTMLへ含めます。LINE 053: ページ全体のフォント・背景・余白を設定。このCSS行で見やすさと操作箇所の判別しやすさを整えます。
LINE 054: 中央寄せする最大幅と余白を設定。このCSS行で見やすさと操作箇所の判別しやすさを整えます。LINE 055: 見出しの大きさと文字色を設定。
このCSS行で見やすさと操作箇所の判別しやすさを整えます。LINE 056: カードの背景・角丸・内側余白を設定。このCSS行で見やすさと操作箇所の判別しやすさを整えます。
LINE 057: カードの薄い影を設定。このCSS行で見やすさと操作箇所の判別しやすさを整えま
コード全体は大きく3つのブロックに分かれます。①アプリ本体とtodos辞書などの保存領域、②一覧HTMLを組み立てる_render_page関数、③追加・完了・削除を受け付けるルーティングです。
一覧表示はGET /がHTMLを返し、POST /add・/toggle/{todo_id}・/delete/{todo_id}の3つが操作を受け持ちます。3操作はいずれも処理後にstatus_code=303のRedirectResponseで一覧へ戻る作りです。
フォームの値はFastAPIのFormで受け取ります。全部で100行程度の見通しのよいコードなので、ここでは「押さえておけば全体が読める」重要な断片だけを順に見ていきましょう。
このセクションの用語
- ルーティング
- URLとHTTPメソッドの組み合わせを、どの関数で処理するか対応付けること。FastAPIでは
@app.get("/")のようなデコレータで指定します。 - f文字列
- 文字列の前に
fを付け、{変数}の形で値を埋め込めるPythonの記法。今回はHTML全体をこれで組み立てています。 - エスケープ
-
<や&などHTMLで特別な意味を持つ文字を、無害な表記(<など)へ変換すること。 - ジェネレータ式
-
sum(1 for t in ...)のように、リストを作らず要素を順に生み出す書き方。件数の集計などを1行で書けます。
"""FastAPIで作るインメモリTODO Webアプリ。"""
import html
from itertools import count
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse, RedirectResponse
# アプリ本体とインメモリの保存領域
app = FastAPI(title="インメモリTODOアプリ")
todos: dict[int, dict] = {}
_next_id = count(1)
def _add_todo(title: str, done: bool = False) -> None:
todo_id = next(_next_id)
todos[todo_id] = {"id": todo_id, "title": title, "done": done}
# 動作イメージが分かる初期タスク(サーバを再起動すると消えるインメモリデータ)
_add_todo("FastAPIでTODOアプリを作る", done=True)
_add_todo("タスクの追加と完了を試す")
_add_todo("いらないタスクを削除する")
# 一覧画面のHTML組み立て
def _render_page() -> str:
rows = []
for todo in todos.values():
title_html = html.escape(todo["title"])
state_class = "done" if todo["done"] else ""
toggle_label = "戻す" if todo["done"] else "完了"
todo_id = todo["id"]
rows.append(
f'<li class="item {state_class}">'
f'<span class="title">{title_html}</span>'
f'<span class="actions">'
f'<form method="post" action="/toggle/{todo_id}">'
f'<button class="btn">{toggle_label}</button></form>'
f'<form method="post" action="/delete/{todo_id}">'
f'<button class="btn danger">削除</button></form>'
f"</span></li>"
)
remaining = sum(1 for t in todos.values() if not t["done"])
items = "\n".join(rows) if rows else '<li class="empty">タスクはありません</li>'
return f"""<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>インメモリTODOアプリ</title>
<style>
body {{ font-family: sans-serif; background: #eef2f7; margin: 0; }}
.wrap {{ max-width: 560px; margin: 40px auto; padding: 0 16px; }}
h1 {{ font-size: 22px; color: #1f2a44; }}
.card {{ background: #fff; border-radius: 12px; padding: 20px;
box-shadow: 0 2px 10px rgba(31, 42, 68, .08); }}
.addform {{ display: flex; gap: 8px; margin-bottom: 16px; }}
.addform input {{ flex: 1; padding: 10px; border: 1px solid #c6cfdd;
border-radius: 8px; font-size: 15px; }}
ul {{ list-style: none; margin: 0; padding: 0; }}
.item {{ display: flex; justify-content: space-between; align-items: center;
padding: 10px 4px; border-bottom: 1px solid #e6ebf2; gap: 8px; }}
.item.done .title {{ text-decoration: line-through; color: #9aa5b5; }}
.actions {{ display: flex; gap: 6px; }}
.actions form {{ margin: 0; }}
.btn {{ padding: 6px 12px; border: none; border-radius: 6px; cursor: pointer;
background: #2f6fed; color: #fff; font-size: 13px; }}
.btn.danger {{ background: #d64545; }}
.addform .btn {{ font-size: 15px; }}
.count {{ color: #5b6779; font-size: 14px; margin-top: 12px; }}
</style>
</head>
<body>
<div class="wrap">
<h1>インメモリTODOアプリ</h1>
<div class="card">
<form class="addform" method="post" action="/add">
<input name="title" placeholder="やることを入力" autocomplete="off" required>
<button class="btn">追加</button>
</form>
<ul>
{items}
</ul>
<p class="count">未完了のタスク: {remaining}件</p>
</div>
</div>
</body>
</html>"""
# 一覧画面の表示
@app.get("/", response_class=HTMLResponse)
def index() -> str:
return _render_page()
# タスクの追加(追加後は303リダイレクトで一覧へ戻す)
@app.post("/add")
def add(title: str = Form(...)) -> RedirectResponse:
cleaned = title.strip()
if cleaned:
_add_todo(cleaned)
return RedirectResponse("/", status_code=303)
# 完了と未完了の切り替え
@app.post("/toggle/{todo_id}")
def toggle(todo_id: int) -> RedirectResponse:
todo = todos.get(todo_id)
if todo is not None:
todo["done"] = not todo["done"]
return RedirectResponse("/", status_code=303)
# タスクの削除
@app.post("/delete/{todo_id}")
def delete(todo_id: int) -> RedirectResponse:
todos.pop(todo_id, None)
return RedirectResponse("/", status_code=303)
# 動作確認したい画面
ROUTES = ["/"]コード全文は上の折り畳みに入れてあるので、全部を上から読む必要はありません。ここでは特に重要な部分だけを抜き出して、何をしているのか順番に見ていきます。
todos辞書とcountによるID採番
app = FastAPI(title="インメモリTODOアプリ")
todos: dict[int, dict] = {}
_next_id = count(1)appがFastAPIアプリの本体で、ここにURLごとの処理を登録していきます。タスクの保存先はただのPython辞書todosで、count(1)は呼ぶたびに1、2、3…と番号を返すカウンタです。この2つがDBの代わりを務めます。
_add_todoでタスクを1件登録する
def _add_todo(title: str, done: bool = False) -> None:
todo_id = next(_next_id)
todos[todo_id] = {"id": todo_id, "title": title, "done": done}next(_next_id)で新しいIDを1つ取り出し、それをキーにして辞書へタスクを追加します。タスク自体もtitleとdoneを持つ小さな辞書で、DBでいう1レコードに相当する形です。
起動時に入れる初期タスク
_add_todo("FastAPIでTODOアプリを作る", done=True)
_add_todo("タスクの追加と完了を試す")
_add_todo("いらないタスクを削除する")動作イメージがすぐつかめるよう、起動時にサンプルを3件登録しています。インメモリなのでサーバを再起動するたびに、この初期状態へ戻る点も覚えておきましょう。
html.escapeでタスク名を無害化
title_html = html.escape(todo["title"])
state_class = "done" if todo["done"] else ""
toggle_label = "戻す" if todo["done"] else "完了"ユーザー入力をそのままHTMLに埋め込むとスクリプトを仕込まれる恐れがあるため、html.escapeで<や>を安全な表記に変換します。あわせて完了状態に応じ、CSSクラスとボタンの文言も切り替えています。
f文字列で1件分のliタグを組み立てる
rows.append(
f'<li class="item {state_class}">'
f'<span class="title">{title_html}</span>'
f'<span class="actions">'
f'<form method="post" action="/toggle/{todo_id}">'
f'<button class="btn">{toggle_label}</button></form>'タスク1件を<li>タグの文字列として組み立て、完了・削除のボタンをそれぞれ独立した<form>にしています。ボタンを押すと/toggle/3のようにIDが入ったURLへPOSTされる仕組みです。
残り件数の集計と空メッセージ
remaining = sum(1 for t in todos.values() if not t["done"])
items = "\n".join(rows) if rows else '<li class="empty">タスクはありません</li>'sumとジェネレータ式で未完了タスクの件数を数えています。タスクが1件もないときは、空のリストの代わりに「タスクはありません」という案内を表示する分岐です。
PythonのFastAPIで作ったTODOアプリの動作確認
完成したコードを保存し、開発サーバを起動してブラウザでhttp://127.0.0.1:8000を開くと、一覧画面が表示されました。レスポンスはHTTP 200で、コード内で登録した初期タスク3件が並んだ状態から始まります。
実際に1種類の画面で、表示と絞り込みの動作を確認できました。追加フォームと完了・削除ボタンがすべて同じページに収まっているため、画面遷移で迷うことがありません。
操作の様子は、実セッションを記録したブラウザ録画としても残しています。どの操作も303リダイレクトで一覧へ戻る作りなので、リロード時にフォーム再送信の警告に悩まされない点もあわせて確認できます。
このセクションの用語
- uvicorn
- FastAPIアプリを動かすためのASGIサーバ。
uvicorn main:app --reloadのように起動して使います。 - ステータスコード
- HTTPレスポンスの結果を表す3桁の数字。200は成功、303は「別のURLをGETで見に行け」という指示を意味します。


PythonのFastAPIによるTODOアプリ開発で起きやすいエラーと対処法
構成はシンプルですが、初めてFastAPIを触るときに引っかかりやすい定番のエラーがいくつかあります。メッセージの読み方さえ分かれば数分で解決できるものばかりなので、先回りして対処を押さえておきましょう。
特にpython-multipartまわりは、FastAPI本体を入れただけでは足りない点が見落とされがちです。フォームを使う場合の追加インストールは、公式ドキュメントにも次のように明記されています。
このセクションの用語
- 仮想環境
- プロジェクトごとにPythonのパッケージ一式を分けて管理する仕組み。
venvで作成し、有効化してからpip installします。 - ポート
- 1台のマシン上で通信の窓口を区別する番号。開発サーバは既定で8000番を使うため、番号が重複すると起動に失敗します。
| エラー例 | 原因 | 対処 |
|---|---|---|
ModuleNotFoundError: No module named 'fastapi' |
FastAPIをインストールした環境と、実行している環境が別になっている | 仮想環境を有効化してからpip install fastapi uvicornを実行し、同じ環境でサーバを起動する |
Form data requires "python-multipart" to be installed |
フォームの受け取りに必要なpython-multipartが未インストール |
pip install python-multipartを追加で実行し、サーバを再起動する |
[Errno 10048]などで起動に失敗する |
既定の8000番ポートを別のプロセス(前回のサーバなど)が使用中 | 前のサーバを停止するか、--port 8001のように別のポートを指定して起動する |
405 Method Not Allowed |
/addなどPOST専用のURLを、ブラウザのアドレス欄から直接GETで開いた |
操作は必ず一覧画面のフォームのボタンから行う。URL直打ちで試すのはGET /だけにする |
NameErrorやInvalid format specifier
|
f文字列の中に書いたCSSの{が、変数の埋め込み記号として解釈された |
HTML内のCSSの波括弧を{{と}}に二重化してエスケープする |
インメモリ構成でつまずきやすい点
エラーとまでは言えないものの、初心者の手が「あれ?」と止まりやすいポイントもあります。どれも今回の最小構成ならではの性質なので、理由とセットで覚えておくと応用が利きます。
さらに詳しく学べるPython研修の詳細はこちら参考:
©FastAPI公式ドキュメントTo use forms, first install python-multipart.
PythonのFastAPIで作るミニWebアプリの活用例
「再起動で消えるアプリに使い道はあるのか」と思うかもしれませんが、消えてよいデータや作り捨ての場面では、この身軽さこそが武器になります。
ポイントは、todos辞書のまわりだけを差し替えれば本格構成へ育てられることです。用途に応じて、次のような使い方が考えられます。
| 使える場面 | 具体的な使い方 |
|---|---|
| チーム内の一時的なタスク共有 | 社内ネットワークで起動し、当日の作業リストや持ち物チェックを共有する。消えてよい情報に限れば、DBの準備なしで数分で使い始められる |
| Webアプリ入門の学習教材 | GETとPOST、フォーム送信、リダイレクトという基本の流れを1ファイルで追体験する。研修ならtodos辞書をDBに置き換える発展課題にもつなげやすい |
| 本格開発前のプロトタイプ | 画面構成と操作の流れをまず動くもので確認し、関係者の合意が取れてから保存部分だけをSQLiteなどへ差し替える |
| 簡易管理画面のたたき台 | 「一覧+操作ボタン」という構造をそのまま流用し、todosを設定値や登録データに置き換えて社内ツールの管理画面にする |
PythonのFastAPIによるTODOアプリ開発のまとめ
FastAPIと標準ライブラリだけで、追加・完了・削除ができるTODOアプリを1ファイルで動かすところまで確認できました。辞書とitertools.countという基本的な道具だけでも、Webアプリの骨格は十分に表現できます。
特に押さえてほしいのは、PRGパターンとhtml.escapeの2点です。アプリの規模が大きくなっても必ず必要になる考え方なので、この最小構成のうちに手を動かして体で覚えておくと後が楽になります。
次の一歩としては、todos辞書をSQLiteに置き換えて永続化したり、HTML組み立てをJinja2のようなテンプレートエンジンへ移行したりする拡張がおすすめです。今回のコードが小さい分、どこを差し替えればよいかも見通しやすいはずです。
参考にした一次情報
- ^ FastAPI公式ドキュメント(日本語). https://fastapi.tiangolo.com/ja/, (参照26-07-18).
- ^ FastAPI公式チュートリアル: フォームデータ. https://fastapi.tiangolo.com/ja/tutorial/request-forms/, (参照26-07-18).
- ^ Uvicorn公式ドキュメント. https://www.uvicorn.org/, (参照26-07-18).
- ^ MDN Web Docs: 303 See Other. https://developer.mozilla.org/ja/docs/Web/HTTP/Reference/Status/303, (参照26-07-18).
※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。
ITやプログラミングに関するコラム
【CSS】notで複数の件を除外する方法
【Git】remote設定を変更する方法
【VBA】コメントアウトを設定する方法
x86とx64の違いを分かりやすく解説
マークダウンで改行する方法
CapsLockキーを解除する方法
GitLabとGitHubの違いを解説
Linuxで環境変数を確認する方法
UbuntuのIPアドレスを確認する方法
パソコンのメモリの目安を用途別に選ぶ方法
ITやプログラミングに関するニュース
VercelがAI GatewayにSeedream 5.0 Proを追加、AI SDKのモデル指定で画像生成と編集が可能に
AWSがAmazon LocationのPlaces APIを強化、住所表記の指定と移動手段別の検索が可能に
VercelがトレースにTree・Waterfallビューを追加、ログ画面で処理の階層と所要時間を確認可能に
Googleがエージェント評価の再考を提唱、難易度を情報量で測るDiscovery Benchを解説
Google CloudがCloud Runサンドボックスを公開プレビューで提供、サービスヘルスは一般提供に
Google Cloud EMEAが英国金融の重要第三者に指定、イングランド銀行・PRA・FCAの直接監督下に
AWS DMS Schema ConversionがSQL Serverのオフライン変換に対応、ソースDBへ接続せずスキーマを変換可能に
EC2 G7インスタンスが米国東部(バージニア北部)で利用可能に、G6比でAI推論性能が最大4.6倍
SageMaker HyperPodが継続プロビジョニングでのAMIベース構成に対応、S3のスクリプト管理なしでSlurmクラスターを作成可能に
AWSがEMR on EKSでSparkトラブルシューティングエージェントに対応、失敗ジョブの原因分析を自然言語で依頼可能に
