【Python】FastAPIでインメモリTODO Webアプリを作ってみた

【Python】FastAPIでインメモリTODO Webアプリを作ってみた

公開: 更新:
CodeCampが提供するDX人材育成が可能なプログラミングやITが学べる公開講座

PythonのWebフレームワークFastAPIを使って、ブラウザから操作できるTODOアプリを1ファイルで作ってみました。データベースもテンプレートエンジンも使わず、タスクは辞書に保存するインメモリ構成です。追加・完了・削除の3操作はフォームとリダイレクトだけで実現し、リロードしても二重送信されないPRGパターンも取り入れました。

FastAPIの基本だけでWebアプリがちゃんと動くところまでを、コードを断片ごとに分解しながら初心者向けに解説します。

FastAPIの基本概念、要件定義、実装、動作確認までを順番に学べます。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。

【Python】FastAPIでインメモリTODO Webアプリを作ってみた|フルカリキュラム|概要・完成挙動
動画の内容をテキストで確認する

オープニング。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」とは

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研修の詳細はこちら

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エスケープしてから表示する

完成と判断する条件

  1. 起動直後に初期TODOが3件表示される
  2. 未完了件数が2件と表示される
  3. 新しいTODOを追加すると一覧と件数へ反映される
  4. 追加したTODOを完了にすると取り消し線が付き件数が減る
  5. 削除操作後に対象TODOが一覧から消える
さらに詳しく学べるPython研修の詳細はこちら

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研修の詳細はこちら

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研修の詳細はこちら

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研修の詳細はこちら

PythonのFastAPIでTODOアプリを段階的に実装する流れ

最初から全機能を一度に書くのではなく、役割ごとに小さく組み立てます。

各段階で確認する内容を決めておくと、問題が起きた場所を特定しやすくなります。

実装する順序

  1. 必要なモジュールを読み込み、FastAPIアプリとTODO保存用の辞書を用意する
  2. TODOを1件追加する共通関数と、初期表示用のデータを作る
  3. TODO一覧を安全なHTMLへ変換する関数を作る
  4. 一覧表示、追加、完了切り替え、削除のルートを定義する
  5. ブラウザから一連の操作を行い、表示と件数の変化を確認する

各段階で確認すること

次の確認を順番に行い、完成状態まで機能を積み上げます。

  1. 依存関係を読み込めるか確認:FastAPI、Form、2種類のレスポンス型を実際にimportします。
  2. 最小のTODOを1件保存:_add_todo()を呼び、連番IDと辞書への保存を確認します。
  3. 初期データの件数を確認:起動時に3件が登録され、そのうち未完了が2件であることを確認します。
  4. HTMLを最後まで生成できるか確認:_render_page()を実行し、HTML文字列とTODO行が生成されることを確認します。
  5. 一覧表示ルートを確認:FastAPIへGET /が登録され、HTML画面を返す準備ができたか確認します。
  6. 追加処理と303遷移を確認:add()を直接呼び、TODOが増えて303レスポンスになることを確認します。
  7. 完了状態の反転を確認:toggle()で未完了のタスクが完了へ切り替わることを確認します。
  8. 削除処理を確認:delete()で指定IDが辞書から消え、303で一覧へ戻ることを確認します。
  9. 完成した全ルートを確認:一覧・追加・完了切替・削除の4ルートが登録済みか最終確認します。

Python研修一覧はこちら

目的に合うPython研修を一覧形式から探したい方は、ぜひご利用ください。

Python研修を比較する

Java研修一覧はこちら

目的に合うJava研修を一覧形式から探したい方は、ぜひご利用ください。

Java研修を比較する

PHP研修一覧はこちら

目的に合うPHP研修を一覧形式から探したい方は、ぜひご利用ください。

PHP研修を比較する

新入社員研修

目的に合う新入社員研修を一覧形式から探したい方は、ぜひご利用ください。

新入社員研修を比較する

全ての研修からも探したい方はこちら

PythonのFastAPIで作るTODOアプリの完成コード

00:00 / 11:43
【Python】FastAPIでインメモリTODO Webアプリを作ってみた|フルカリキュラム|コード実装
動画の内容をテキストで確認する

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=303RedirectResponseで一覧へ戻る作りです。

フォームの値はFastAPIのFormで受け取ります。全部で100行程度の見通しのよいコードなので、ここでは「押さえておけば全体が読める」重要な断片だけを順に見ていきましょう。

このセクションの用語

ルーティング
URLとHTTPメソッドの組み合わせを、どの関数で処理するか対応付けること。FastAPIでは@app.get("/")のようなデコレータで指定します。
f文字列
文字列の前にfを付け、{変数}の形で値を埋め込めるPythonの記法。今回はHTML全体をこれで組み立てています。
エスケープ
<&などHTMLで特別な意味を持つ文字を、無害な表記(&lt;など)へ変換すること。
ジェネレータ式
sum(1 for t in ...)のように、リストを作らず要素を順に生み出す書き方。件数の集計などを1行で書けます。
_render_pageが一覧HTMLを組み立てるデータの流れ todos辞書からタスクを順に取り出すhtml.escapeでタスク名を無害化するf文字列で1件ずつ<li>タグに変換して連結ページ全体のHTML文字列を組み立てるHTMLResponseとしてブラウザへ返す
_render_pageが一覧HTMLを組み立てるデータの流れ
"""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つ取り出し、それをキーにして辞書へタスクを追加します。タスク自体もtitledoneを持つ小さな辞書で、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基礎・実践(Django)

企業・法人向けのPython研修では、基礎から応用まで体系的に学べます。

Python研修の詳細

DX社員研修

企業・法人向けのDX研修では、実務に繋がるリスキリングでITレベルを向上させます。

DX研修の詳細

Javaエンジニア育成研修

企業・法人向けのJavaエンジニア育成研修では、Javaの基礎から応用まで確実に習得できます。

Java研修の詳細

新卒・新入社員向け研修

企業・法人に新入社員・新卒社員に向けたプログラミング研修を提供しています。

新入社員研修の詳細

コードキャンプのIT研修を全て見る

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で見に行け」という指示を意味します。
画面: / (HTTP 200)
画面: / (HTTP 200)
入力欄へ文字を1文字ずつ入力し、追加ボタンを押してTODOが増えるまでの実操作
入力欄へ文字を1文字ずつ入力し、追加ボタンを押してTODOが増えるまでの実操作
さらに詳しく学べるPython研修の詳細はこちら

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 /だけにする
NameErrorInvalid format specifier f文字列の中に書いたCSSの{が、変数の埋め込み記号として解釈された HTML内のCSSの波括弧を{{}}に二重化してエスケープする

インメモリ構成でつまずきやすい点

エラーとまでは言えないものの、初心者の手が「あれ?」と止まりやすいポイントもあります。どれも今回の最小構成ならではの性質なので、理由とセットで覚えておくと応用が利きます。

参考:

©FastAPI公式ドキュメント

To use forms, first install python-multipart.

さらに詳しく学べるPython研修の詳細はこちら

PythonのFastAPIで作るミニWebアプリの活用例

「再起動で消えるアプリに使い道はあるのか」と思うかもしれませんが、消えてよいデータや作り捨ての場面では、この身軽さこそが武器になります。

ポイントは、todos辞書のまわりだけを差し替えれば本格構成へ育てられることです。用途に応じて、次のような使い方が考えられます。

使える場面 具体的な使い方
チーム内の一時的なタスク共有 社内ネットワークで起動し、当日の作業リストや持ち物チェックを共有する。消えてよい情報に限れば、DBの準備なしで数分で使い始められる
Webアプリ入門の学習教材 GETとPOST、フォーム送信、リダイレクトという基本の流れを1ファイルで追体験する。研修ならtodos辞書をDBに置き換える発展課題にもつなげやすい
本格開発前のプロトタイプ 画面構成と操作の流れをまず動くもので確認し、関係者の合意が取れてから保存部分だけをSQLiteなどへ差し替える
簡易管理画面のたたき台 「一覧+操作ボタン」という構造をそのまま流用し、todosを設定値や登録データに置き換えて社内ツールの管理画面にする
さらに詳しく学べるPython研修の詳細はこちら

PythonのFastAPIによるTODOアプリ開発のまとめ

FastAPIと標準ライブラリだけで、追加・完了・削除ができるTODOアプリを1ファイルで動かすところまで確認できました。辞書とitertools.countという基本的な道具だけでも、Webアプリの骨格は十分に表現できます。

特に押さえてほしいのは、PRGパターンとhtml.escapeの2点です。アプリの規模が大きくなっても必ず必要になる考え方なので、この最小構成のうちに手を動かして体で覚えておくと後が楽になります。

次の一歩としては、todos辞書をSQLiteに置き換えて永続化したり、HTML組み立てをJinja2のようなテンプレートエンジンへ移行したりする拡張がおすすめです。今回のコードが小さい分、どこを差し替えればよいかも見通しやすいはずです。

さらに詳しく学べるPython研修の詳細はこちら

参考にした一次情報

  1. ^ FastAPI公式ドキュメント(日本語). https://fastapi.tiangolo.com/ja/, (参照26-07-18).
  2. ^ FastAPI公式チュートリアル: フォームデータ. https://fastapi.tiangolo.com/ja/tutorial/request-forms/, (参照26-07-18).
  3. ^ Uvicorn公式ドキュメント. https://www.uvicorn.org/, (参照26-07-18).
  4. ^ MDN Web Docs: 303 See Other. https://developer.mozilla.org/ja/docs/Web/HTTP/Reference/Status/303, (参照26-07-18).

※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。

ITやプログラミングに関するコラム


ITやプログラミングに関するニュース

ブログに戻る

コメントを残す

コメントは公開前に承認される必要があることにご注意ください。

企業・法人向けのIT・プログラミング・生成AI研修を探す、比較する - IT・プログラミングを知って学べるコネクトメディア CodeCampが提供するDX人材育成が可能なプログラミングやITが学べる公開講座 - IT・プログラミングを知って学べるコネクトメディア コードキャンプが提供する無料で学べるプログラミングスクール講座 - IT・プログラミングを知って学べるコネクトメディア コードキャンプDX人材育成研修 - IT・プログラミングを知って学べるコネクトメディア 3.5日の研修で、年間1,600時間の削減効果が見込まれる。東京きらぼしフィナンシャルグループのDX人材育成事例 - IT・プログラミングを知って学べるコネクトメディア 配属3ヶ月で30%の生産性向上を実現するいよぎんコンピュータサービスの新人研修に迫る - IT・プログラミングを知って学べるコネクトメディア 金融業界の業務効率化を加速するニッセイアセットマネジメントの生成AI×GAS活用研修事例 - IT・プログラミングを知って学べるコネクトメディア 【製造業のDX人材育成事例】デジタル人材の即戦力化を実現する、日本ガイシ株式会社の異動者向オンボーディング研修 - ITやプログラミングを知って学べるコネクトメディア フューチャーアーキテクト株式会社が実現した新入社員向けIT研修プログラムでタスクフォース制度が主体的な学びと成長を生み出す - IT・プログラミングを知って学べるコネクトメディア コードキャンプDX人材育成研修 - IT・プログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/【IT新入社員研修】オンラインとオフラインの最適バランスを実現したFutureOneの導入事例 - IT・プログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/【新入社員研修】柔軟なハイブリッド型Java研修で実現した新卒20名の成長と成果|サークレイス株式会社 - ITやプログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/現場により近いところにデジタルを根付かせるDX基礎講座研修|株式会社ブリヂストン - ITやプログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/業務の効率化・DX推進に向けたIT人材育成への第一歩|株式会社カナエ - ITやプログラミングを知って学べるコネクトメディア 企業・法人向けのIT・プログラミング研修 - ITやプログラミングを知って学べるコネクトメディア

新着記事

対象者別で探す

子供(小学生・中学生・高校生)向け
プログラミング教室検索する

子供(小学生・中学生・高校生)がロボットやプログラミング言語を学ぶことができるオフラインからオンラインスクールを検索、比較することが可能です。

子供(小学生・中学生・高校生)
プログラミング教室検索する

ITやプログラムなどの
最新情報を検索する

日々、新しいITやプログラミング言語の情報が流れていきますが、特定の情報を時系列でニュースやコラムを確認することができます。

ITやプログラムなどの
最新情報を検索する