Python | 1 日 120 分 × 7 日アプリ学習:Flaskで作る超簡単Webアプリ(中級編)

Web APP Python
スポンサーリンク

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アプリ”の入り口には完全に立っているよ。

タイトルとURLをコピーしました