7日目のゴール
7日目のテーマは
「Flask の Webサーバーとルーティングを、自分で“ゼロから設計できるレベル”まで持っていくこと」 です。
ここまでで、あなたはすでに
固定パス、動的パス、クエリパラメータ
GET と POST、フォーム送信、リダイレクト
一覧・詳細・作成・削除という一通りの流れ
を一度、自分の手で通っています。
7日目では、
「講師が用意した設計をなぞる」のではなく、
あなた自身が“どんなURLにして、どんな動きをさせるか”を考える側に回る ことを意識します。
題材として、6日目のメモアプリとよく似た
「タスク管理ミニアプリ」を作りながら、
ルーティング設計の考え方をもう一段深く固めていきます。
ここまでのルーティングを一言でまとめる
Flask のルーティングは、ずっとこの形でした。
URL(パス)を決める
そのURLに対して、どのHTTPメソッドを受け付けるかを決める
そのURLとメソッドの組み合わせに対して、どの関数を呼ぶかを決める
関数の返り値(文字列やHTML)が、そのままレスポンスになる
そして、URLの中に
パスパラメータ(/items/<int:id> の <int:id>)
クエリパラメータ(?q=xxx&page=2 の q や page)
を埋め込むことで、
「同じ種類のページだけど、中身が違う」ものを表現してきました。
7日目は、この考え方を
「一つのアプリ全体の設計」にまで引き上げます。
今日作るものを“言葉だけ”で設計してみる
タスク管理ミニアプリの仕様を文章で決める
まずは、コードを書かずに「どんなアプリにするか」を言葉で決めます。
やりたいことはシンプルです。
タスクの一覧を見たい
タスクを追加したい
タスクを「完了」にしたり「未完了」に戻したりしたい
これをURLに落とし込んでいきます。
タスク一覧GET /tasks
タスク新規作成フォーム表示GET /tasks/new
タスク新規作成送信POST /tasks/new
タスク詳細GET /tasks/<int:task_id>
タスクの完了状態を切り替えPOST /tasks/<int:task_id>/toggle
さらに、一覧ページで
「全部」「完了だけ」「未完了だけ」
を切り替えられるようにしたいので、クエリパラメータも使います。
GET /tasks?status=allGET /tasks?status=doneGET /tasks?status=todo
ここまで言葉で書けたら、もう設計の8割は終わっています。
あとは、この設計図を Flask のコードに写していくだけです。
疑似データベースとしてのタスクリストを用意する
Pythonのリストと辞書でタスクを表現する
まずは、アプリの土台となるデータ構造を決めます。
from flask import Flask, request, redirect, url_for
app = Flask(__name__)
tasks = []
next_task_id = 1
Pythonタスク1件は、次のような辞書で表現します。
task = {
"id": 1,
"title": "牛乳を買う",
"done": False,
}
Pythonid は一意な番号title はタスクの内容done は完了しているかどうか(True / False)
tasks は、こういう辞書を並べたリストです。next_task_id は、新しいタスクを作るたびに1ずつ増やしていきます。
これは、6日目のメモアプリと同じ構造です。
「リソースがメモからタスクに変わっただけ」と捉えてOKです。
タスク一覧ページのルーティングと実装
GET /tasks と ?status= で絞り込み
まずは一覧ページから作ります。
ここで、クエリパラメータを使って絞り込みも実装します。
@app.route("/tasks")
def list_tasks():
status = request.args.get("status", "all")
if status == "done":
filtered = [t for t in tasks if t["done"]]
heading = "完了したタスク一覧"
elif status == "todo":
filtered = [t for t in tasks if not t["done"]]
heading = "未完了のタスク一覧"
else:
filtered = tasks
heading = "すべてのタスク一覧"
lines = []
lines.append(f"<h1>{heading}</h1>")
lines.append('<p><a href="/tasks/new">新しいタスクを追加</a></p>')
lines.append('<p>')
lines.append('<a href="/tasks?status=all">すべて</a> | ')
lines.append('<a href="/tasks?status=todo">未完了</a> | ')
lines.append('<a href="/tasks?status=done">完了</a>')
lines.append('</p>')
if not filtered:
lines.append("<p>該当するタスクはありません。</p>")
else:
lines.append("<ul>")
for task in filtered:
detail_url = url_for("show_task", task_id=task["id"])
mark = "✅" if task["done"] else "⬜"
lines.append(f'<li>{mark} <a href="{detail_url}">{task["title"]}</a></li>')
lines.append("</ul>")
lines.append('<p><a href="/">トップに戻る</a></p>')
return "\n".join(lines)
Pythonここでの重要ポイントを深掘りします。
status = request.args.get("status", "all")
クエリパラメータ status を受け取り、指定がなければ "all" を使います。
これで、同じ /tasks というURLでも、?status=todo や ?status=done で中身を切り替えられます。
filtered に「表示対象のタスクだけ」を入れるstatus の値に応じて、tasks から絞り込みをしています。
この「一覧+絞り込み」は、実務でも頻出のパターンです。
url_for("show_task", task_id=task["id"])
詳細ページのURLを、関数名と引数から生成しています。
URLを文字列でベタ書きしないことで、後からルートを変えても壊れにくくなります。
タスク新規作成のルーティングと実装
GET /tasks/new と POST /tasks/new
次に、新しいタスクを追加するページを作ります。
@app.route("/tasks/new", methods=["GET", "POST"])
def new_task():
global next_task_id
if request.method == "GET":
return """
<h1>新しいタスクを追加</h1>
<form method="post">
<p><input type="text" name="title" placeholder="タスク内容"></p>
<p><button type="submit">追加</button></p>
</form>
<p><a href="/tasks">タスク一覧に戻る</a></p>
"""
title = request.form.get("title", "").strip()
if not title:
return """
<h1>エラー</h1>
<p>タスク内容は必須です。</p>
<p><a href="/tasks/new">戻る</a></p>
"""
task = {
"id": next_task_id,
"title": title,
"done": False,
}
tasks.append(task)
next_task_id += 1
return redirect(url_for("list_tasks"))
Pythonここでの本質を整理します。
methods=["GET", "POST"] で、同じURLでフォーム表示と送信後処理を切り替えている
GET のときはフォームを返し、POST のときはデータを受け取って保存します。
request.form.get("title") でフォームの値を受け取る
HTML側の name="title" と対応しています。strip() で前後の空白を削り、空文字を弾いています。
保存後は redirect(url_for("list_tasks"))
5日目でやった「POST/Redirect/GET」パターンです。
追加後にリロードしても、同じPOSTが二重送信されません。
タスク詳細ページのルーティングと実装
GET /tasks/<int:task_id>
次は、1件のタスクを表示するページです。
@app.route("/tasks/<int:task_id>")
def show_task(task_id):
task = None
for t in tasks:
if t["id"] == task_id:
task = t
break
if task is None:
return """
<h1>タスクが見つかりません</h1>
<p>指定されたIDのタスクは存在しません。</p>
<p><a href="/tasks">タスク一覧に戻る</a></p>
""", 404
toggle_url = url_for("toggle_task", task_id=task_id)
status_text = "完了" if task["done"] else "未完了"
button_label = "未完了に戻す" if task["done"] else "完了にする"
return f"""
<h1>タスク詳細</h1>
<p>ID: {task["id"]}</p>
<p>内容: {task["title"]}</p>
<p>状態: {status_text}</p>
<form method="post" action="{toggle_url}">
<button type="submit">{button_label}</button>
</form>
<p><a href="/tasks">タスク一覧に戻る</a></p>
"""
Pythonここでの重要ポイントを深掘りします。
@app.route("/tasks/<int:task_id>")
URLの一部を「タスクID」として受け取っています。/tasks/1 なら task_id = 1 です。
tasks リストから、対応するIDのタスクを探している
見つからなければ404を返します。
「存在しないIDにアクセスされたときの振る舞い」も、ルーティング設計の一部です。
完了状態を切り替えるフォームの action に、toggle_task のURLを埋め込んでいる
ここで、次に作る「完了状態切り替え用ルート」とつながります。
タスク完了状態の切り替えルーティングと実装
POST /tasks/<int:task_id>/toggle
完了・未完了の切り替えは、
「状態を変える操作」なので POST で行います。
@app.route("/tasks/<int:task_id>/toggle", methods=["POST"])
def toggle_task(task_id):
task = None
for t in tasks:
if t["id"] == task_id:
task = t
break
if task is None:
return """
<h1>タスクが見つかりません</h1>
<p>すでに削除された可能性があります。</p>
<p><a href="/tasks">タスク一覧に戻る</a></p>
""", 404
task["done"] = not task["done"]
return redirect(url_for("show_task", task_id=task_id))
Pythonここでの本質を整理します。
methods=["POST"] で、このURLはPOST専用にしている
GET で直接叩いて状態を変えられないようにしています。
「状態を変える操作はPOST」というのは、Web設計の基本的な考え方です。
タスクが見つからなければ404
「すでに削除されたかもしれない」というメッセージを出しています。
状態を切り替えたあと、詳細ページにリダイレクト
ここでも「POST/Redirect/GET」パターンです。
切り替え後の状態を GET のページで確認できるようにしています。
トップページと404カスタムを足して“アプリの顔”にする
/ と 404 を整える
最後に、トップページと404ページを用意して、
アプリ全体の顔を整えます。
@app.route("/")
def index():
return """
<h1>Flask タスク管理アプリ(7日目)</h1>
<p><a href="/tasks">タスク一覧を見る</a></p>
"""
@app.errorhandler(404)
def page_not_found(error):
return """
<h1>404 - ページが見つかりません</h1>
<p>URLが間違っているか、このページは存在しません。</p>
<p><a href="/">トップに戻る</a></p>
""", 404
Pythonこれで、
存在しないURLにアクセスされたときも、
アプリらしいメッセージで案内できるようになります。
全体コードを一つにまとめる
ここまでのコードを一つにすると、こうなります。
from flask import Flask, request, redirect, url_for
app = Flask(__name__)
tasks = []
next_task_id = 1
@app.route("/")
def index():
return """
<h1>Flask タスク管理アプリ(7日目)</h1>
<p><a href="/tasks">タスク一覧を見る</a></p>
"""
@app.route("/tasks")
def list_tasks():
status = request.args.get("status", "all")
if status == "done":
filtered = [t for t in tasks if t["done"]]
heading = "完了したタスク一覧"
elif status == "todo":
filtered = [t for t in tasks if not t["done"]]
heading = "未完了のタスク一覧"
else:
filtered = tasks
heading = "すべてのタスク一覧"
lines = []
lines.append(f"<h1>{heading}</h1>")
lines.append('<p><a href="/tasks/new">新しいタスクを追加</a></p>')
lines.append('<p>')
lines.append('<a href="/tasks?status=all">すべて</a> | ')
lines.append('<a href="/tasks?status=todo">未完了</a> | ')
lines.append('<a href="/tasks?status=done">完了</a>')
lines.append('</p>')
if not filtered:
lines.append("<p>該当するタスクはありません。</p>")
else:
lines.append("<ul>")
for task in filtered:
detail_url = url_for("show_task", task_id=task["id"])
mark = "✅" if task["done"] else "⬜"
lines.append(f'<li>{mark} <a href="{detail_url}">{task["title"]}</a></li>')
lines.append("</ul>")
lines.append('<p><a href="/">トップに戻る</a></p>')
return "\n".join(lines)
@app.route("/tasks/new", methods=["GET", "POST"])
def new_task():
global next_task_id
if request.method == "GET":
return """
<h1>新しいタスクを追加</h1>
<form method="post">
<p><input type="text" name="title" placeholder="タスク内容"></p>
<p><button type="submit">追加</button></p>
</form>
<p><a href="/tasks">タスク一覧に戻る</a></p>
"""
title = request.form.get("title", "").strip()
if not title:
return """
<h1>エラー</h1>
<p>タスク内容は必須です。</p>
<p><a href="/tasks/new">戻る</a></p>
"""
task = {
"id": next_task_id,
"title": title,
"done": False,
}
tasks.append(task)
next_task_id += 1
return redirect(url_for("list_tasks"))
@app.route("/tasks/<int:task_id>")
def show_task(task_id):
task = None
for t in tasks:
if t["id"] == task_id:
task = t
break
if task is None:
return """
<h1>タスクが見つかりません</h1>
<p>指定されたIDのタスクは存在しません。</p>
<p><a href="/tasks">タスク一覧に戻る</a></p>
""", 404
toggle_url = url_for("toggle_task", task_id=task_id)
status_text = "完了" if task["done"] else "未完了"
button_label = "未完了に戻す" if task["done"] else "完了にする"
return f"""
<h1>タスク詳細</h1>
<p>ID: {task["id"]}</p>
<p>内容: {task["title"]}</p>
<p>状態: {status_text}</p>
<form method="post" action="{toggle_url}">
<button type="submit">{button_label}</button>
</form>
<p><a href="/tasks">タスク一覧に戻る</a></p>
"""
@app.route("/tasks/<int:task_id>/toggle", methods=["POST"])
def toggle_task(task_id):
task = None
for t in tasks:
if t["id"] == task_id:
task = t
break
if task is None:
return """
<h1>タスクが見つかりません</h1>
<p>すでに削除された可能性があります。</p>
<p><a href="/tasks">タスク一覧に戻る</a></p>
""", 404
task["done"] = not task["done"]
return redirect(url_for("show_task", task_id=task_id))
@app.errorhandler(404)
def page_not_found(error):
return """
<h1>404 - ページが見つかりません</h1>
<p>URLが間違っているか、このページは存在しません。</p>
<p><a href="/">トップに戻る</a></p>
""", 404
if __name__ == "__main__":
app.run(debug=True)
Python7日目で絶対に押さえてほしい本質
ルーティングは、
「URLに対してどの関数を呼ぶか」を決める仕組み、
というだけではありません。
/tasks/tasks/new/tasks/<id>/tasks/<id>/toggle
のように、
URLそのものが「アプリの機能一覧」になっている。
そして、Flaskのルーティングは、その設計図をコードに落とすための言語です。
ここまで7日間やってきたあなたは、
もう「Flaskを触ったことがある人」ではなく、
自分で小さなWebアプリの構造を設計して、ルーティングに落とし込める人 です。
この先は、
テンプレートファイルを使って見た目を整えたり、
データベースを使って本当に保存できるようにしたり、
ログイン機能を足したり。
どこに進んでも、今日までに積み上げた
「URL・メソッド・ルーティング・リダイレクト・エラー」
の感覚が、ずっと土台になり続けます。

