6日目のゴール
6日目のテーマは
「Flaskのルーティングを“ミニアプリ全体の設計”として考えられるようになる」 ことです。
ここまでであなたはすでに
固定URLと動的URLを定義できる
クエリパラメータ(?q=xxx)を扱える
GET と POST を切り替えてフォーム付きページを書ける
リダイレクトや404カスタムも使える
というところまで来ています。
6日目では、ここから一歩進んで、
複数の「リソース」(メモ、ユーザーなど)をURLで整理する
同じリソースに対して「一覧」「詳細」「作成」「削除」をルーティングで表現する
URL設計を先に言葉で考えてからコードに落とす
このあたりを、ミニアプリを通して体に入れていきます。
「ルーティング=ただの書き方」ではなく、
「アプリの構造そのもの」 として見えるようになるのが、今日のゴールです。
まずは「URLを言葉で設計する」感覚を持つ
どんなミニアプリを作るかを先に決める
今日は、次のような「複数メモを扱うミニアプリ」を作ります。
メモの一覧ページ
メモの新規作成ページ
メモの詳細ページ
メモの削除
これを、URLでどう表現するかを先に言葉で決めます。
メモ一覧GET /memos
メモ新規作成フォーム表示GET /memos/new
メモ新規作成送信POST /memos/new
メモ詳細GET /memos/<int:memo_id>
メモ削除POST /memos/<int:memo_id>/delete
この「URLの一覧」が、そのままアプリの設計図になります。
ここまで言葉で描けたら、あとは Flask の @app.route に落としていくだけです。
疑似データベースとしての「メモリ上のリスト」
まずはPythonのリストでメモを管理する
本物のデータベースはまだ使いません。
まずは、Pythonのリストと辞書で「疑似DB」を作ります。
from flask import Flask, request, redirect, url_for
app = Flask(__name__)
memos = []
next_id = 1
Pythonここでのポイントは二つです。
memos は「メモの一覧」を表すリスト
各メモは {"id": 1, "title": "...", "body": "..."} のような辞書にします。
next_id は「次に使うID」を管理するカウンタ
新しいメモを作るたびに1ずつ増やしていきます。
これは、データベースを使う前の「ミニチュア版」だと思ってください。
構造はほぼ同じです。
メモ一覧ページのルーティングと実装
GET /memos で全メモを表示する
まずは一覧ページから作ります。
@app.route("/memos")
def list_memos():
lines = []
lines.append("<h1>メモ一覧</h1>")
lines.append('<p><a href="/memos/new">新しいメモを書く</a></p>')
if not memos:
lines.append("<p>まだメモはありません。</p>")
else:
lines.append("<ul>")
for memo in memos:
detail_url = url_for("show_memo", memo_id=memo["id"])
lines.append(f'<li><a href="{detail_url}">{memo["title"]}</a></li>')
lines.append("</ul>")
lines.append('<p><a href="/">トップに戻る</a></p>')
return "\n".join(lines)
Pythonここでの重要ポイントを深掘りします。
@app.route("/memos") が「メモ一覧のURL」を決めている
このURLにアクセスすると、list_memos 関数が呼ばれます。
memos リストの中身をループして、タイトルをリンクとして表示している
各メモには id があるので、詳細ページへのリンクを url_for("show_memo", memo_id=...) で作っています。
メモが一件もないときは、「まだメモはありません」と表示している
こういう「空のときの表示」も、アプリとしては大事な振る舞いです。
ここで url_for("show_memo", memo_id=memo["id"]) を使っているのがポイントです。
詳細ページのURLを文字列でベタ書きせず、「関数名+引数」から生成しています。
メモ新規作成フォームのルーティングと実装
GET /memos/new と POST /memos/new を同じ関数で扱う
次に、新規作成ページを作ります。
@app.route("/memos/new", methods=["GET", "POST"])
def new_memo():
global next_id
if request.method == "GET":
return """
<h1>新しいメモを書く</h1>
<form method="post">
<p><input type="text" name="title" placeholder="タイトル"></p>
<p><textarea name="body" rows="4" cols="40" placeholder="本文"></textarea></p>
<p><button type="submit">保存</button></p>
</form>
<p><a href="/memos">メモ一覧に戻る</a></p>
"""
title = request.form.get("title", "").strip()
body = request.form.get("body", "").strip()
if not title:
return """
<h1>エラー</h1>
<p>タイトルは必須です。</p>
<p><a href="/memos/new">戻る</a></p>
"""
memo = {
"id": next_id,
"title": title,
"body": body,
}
memos.append(memo)
next_id += 1
return redirect(url_for("list_memos"))
Pythonここでの本質を整理します。
methods=["GET", "POST"] で、同じURLでフォーム表示と送信後処理を切り替えている
GET のときはフォームを表示、POST のときはデータを受け取って保存します。
request.form.get("title") と request.form.get("body") でフォームの値を受け取る
HTML側の name="title" と name="body" に対応しています。
タイトルが空のときはエラーメッセージを返している
「必須項目」をサーバー側でチェックするのは、とても大事な習慣です。
保存が終わったら redirect(url_for("list_memos")) で一覧ページに戻している
これが、5日目でやった「POST/Redirect/GET」パターンです。
新規作成後にリロードしても、同じPOSTが二重送信されません。
ここまでで、「一覧」と「新規作成」がつながりました。
メモ詳細ページのルーティングと実装
GET /memos/<int:memo_id> で1件のメモを表示する
次は、個別のメモを表示するページです。
@app.route("/memos/<int:memo_id>")
def show_memo(memo_id):
memo = None
for m in memos:
if m["id"] == memo_id:
memo = m
break
if memo is None:
return """
<h1>メモが見つかりません</h1>
<p>指定されたIDのメモは存在しません。</p>
<p><a href="/memos">メモ一覧に戻る</a></p>
""", 404
delete_url = url_for("delete_memo", memo_id=memo_id)
return f"""
<h1>{memo["title"]}</h1>
<pre>{memo["body"]}</pre>
<form method="post" action="{delete_url}">
<button type="submit">このメモを削除する</button>
</form>
<p><a href="/memos">メモ一覧に戻る</a></p>
"""
Pythonここでの重要ポイントを深掘りします。
@app.route("/memos/<int:memo_id>") で、URLの一部を「メモID」として受け取っている/memos/1 にアクセスすると memo_id = 1、/memos/5 なら memo_id = 5 になります。
memos リストから、対応するIDのメモを探している
見つからなければ None のままです。
見つからなかったときは、404ステータスでエラーメッセージを返しているreturn "メッセージ", 404 のように、ステータスコードを明示しています。
これにより、ブラウザやクローラにも「本当に存在しない」と伝えられます。
削除ボタンのフォームの action に、delete_memo のURLを url_for で埋め込んでいる
ここで、次に作る「削除用ルート」とつながります。
この時点で、
「一覧 → 詳細」
という流れができました。
メモ削除のルーティングと実装
POST /memos/<int:memo_id>/delete で削除する
削除は、GET ではなく POST で行うのが基本です。
URLとメソッドをはっきり分けておきます。
@app.route("/memos/<int:memo_id>/delete", methods=["POST"])
def delete_memo(memo_id):
global memos
new_memos = []
found = False
for m in memos:
if m["id"] == memo_id:
found = True
else:
new_memos.append(m)
memos = new_memos
if not found:
return """
<h1>削除対象のメモが見つかりません</h1>
<p>すでに削除された可能性があります。</p>
<p><a href="/memos">メモ一覧に戻る</a></p>
""", 404
return redirect(url_for("list_memos"))
Pythonここでの本質を整理します。
methods=["POST"] として、「このURLはPOST専用」と宣言している
ブラウザから直接URLを叩いても、GETではアクセスできません。
削除は「フォームからのPOSTでのみ行う」という設計です。
memos から対象IDのメモを取り除いた新しいリストを作っている
シンプルにするために、new_memos を作って最後に置き換えています。
削除対象が見つからなかったときは404を返している
「削除済みかもしれない」というメッセージを出しています。
削除が成功したら、一覧ページにリダイレクトしている
ここでも「POST/Redirect/GET」パターンを使っています。
これで、
一覧 /memos
新規 /memos/new
詳細 /memos/<id>
削除 /memos/<id>/delete
という「一つのリソースに対する一通りの操作」が揃いました。
全体コードを一つにまとめる
ミニ「メモ管理アプリ」として完成させる
ここまでのコードを一つにまとめると、こうなります。
from flask import Flask, request, redirect, url_for
app = Flask(__name__)
memos = []
next_id = 1
@app.route("/")
def index():
return """
<h1>Flask メモアプリ(6日目)</h1>
<p><a href="/memos">メモ一覧を見る</a></p>
"""
@app.route("/memos")
def list_memos():
lines = []
lines.append("<h1>メモ一覧</h1>")
lines.append('<p><a href="/memos/new">新しいメモを書く</a></p>')
if not memos:
lines.append("<p>まだメモはありません。</p>")
else:
lines.append("<ul>")
for memo in memos:
detail_url = url_for("show_memo", memo_id=memo["id"])
lines.append(f'<li><a href="{detail_url}">{memo["title"]}</a></li>')
lines.append("</ul>")
lines.append('<p><a href="/">トップに戻る</a></p>')
return "\n".join(lines)
@app.route("/memos/new", methods=["GET", "POST"])
def new_memo():
global next_id
if request.method == "GET":
return """
<h1>新しいメモを書く</h1>
<form method="post">
<p><input type="text" name="title" placeholder="タイトル"></p>
<p><textarea name="body" rows="4" cols="40" placeholder="本文"></textarea></p>
<p><button type="submit">保存</button></p>
</form>
<p><a href="/memos">メモ一覧に戻る</a></p>
"""
title = request.form.get("title", "").strip()
body = request.form.get("body", "").strip()
if not title:
return """
<h1>エラー</h1>
<p>タイトルは必須です。</p>
<p><a href="/memos/new">戻る</a></p>
"""
memo = {
"id": next_id,
"title": title,
"body": body,
}
memos.append(memo)
next_id += 1
return redirect(url_for("list_memos"))
@app.route("/memos/<int:memo_id>")
def show_memo(memo_id):
memo = None
for m in memos:
if m["id"] == memo_id:
memo = m
break
if memo is None:
return """
<h1>メモが見つかりません</h1>
<p>指定されたIDのメモは存在しません。</p>
<p><a href="/memos">メモ一覧に戻る</a></p>
""", 404
delete_url = url_for("delete_memo", memo_id=memo_id)
return f"""
<h1>{memo["title"]}</h1>
<pre>{memo["body"]}</pre>
<form method="post" action="{delete_url}">
<button type="submit">このメモを削除する</button>
</form>
<p><a href="/memos">メモ一覧に戻る</a></p>
"""
@app.route("/memos/<int:memo_id>/delete", methods=["POST"])
def delete_memo(memo_id):
global memos
new_memos = []
found = False
for m in memos:
if m["id"] == memo_id:
found = True
else:
new_memos.append(m)
memos = new_memos
if not found:
return """
<h1>削除対象のメモが見つかりません</h1>
<p>すでに削除された可能性があります。</p>
<p><a href="/memos">メモ一覧に戻る</a></p>
""", 404
return redirect(url_for("list_memos"))
@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)
Pythonこれを動かして、ブラウザで
トップ /
一覧 /memos
新規 /memos/new
詳細 /memos/1 など
を行ったり来たりしてみてください。
「URLの設計=アプリの設計」になっている感覚が、かなりはっきり見えてくるはずです。
6日目で絶対に押さえてほしい本質
「ルーティングは“リソースと操作”を表現する言語」
今日いちばん大事なのは、
ルーティングをこういう目で見られるようになることです。
/memos は「メモというリソースの一覧」/memos/new は「メモの新規作成」/memos/<id> は「特定のメモの詳細」/memos/<id>/delete は「特定のメモの削除」
というふうに、
URLそのものが「何をしたいのか」を表現している。
そして、それを Flask の
@app.route("パス", methods=[...])
関数名(list_memos, new_memo, show_memo, delete_memo)url_for("関数名", 引数...)
で、きれいに結びつけていく。
ここまで来たあなたは、
もう「Flaskでページを出せる人」ではなく、
「URL設計から逆算してWebアプリを組み立てられる人」 です。
7日目は、このメモアプリをベースにしてもいいし、
まったく別のリソース(タスク、ブックマーク、日記など)で
同じ構造を自分で設計し直してみるのも面白いと思う。
どっちにしても、もう“本物のWebアプリ”の入り口には完全に立っているよ。

