Python | 1 日 120 分 × 7 日アプリ学習:API取得アプリ(requests使用)(中級編)

Web APP Python
スポンサーリンク

7日目のゴール

7日目のテーマは
「ここまで学んだ requests+API 設計を“1つの完成アプリ”としてまとめる」 ことです。

今日できるようになってほしいのは、次のイメージです。

外部APIと話す「APIクライアント層」
アプリに必要な機能をまとめる「サービス層」
ユーザーと対話する「アプリ層」

この三つを意識して、
「小さな API ダッシュボードアプリ」を完成させます。

使うAPIは、これまでと同じ https://jsonplaceholder.typicode.com です。
ユーザー一覧、TODO、記事をまとめて扱えるコンソールアプリを作ります。


全体像を言葉で設計する

どんなアプリを作るか

7日目で作るのは、
「JSONPlaceholder ダッシュボード(コンソール版)」です。

やりたいことは、こういう感じです。

ユーザー一覧を取得して表示する
ユーザーIDを指定して、その人の TODO を一覧表示する
ユーザーIDを指定して、その人の記事(posts)を一覧表示する
エラーが起きても落ちずにメッセージを出す

これを、
APIクライアント層
サービス層
アプリ層

に分けて書いていきます。


APIクライアント層 request_json の完成形

すべてのHTTP通信をここに閉じ込める

まずは「外部APIと話す窓口」を一か所にまとめます。
ここに requests とエラー処理を全部押し込めて、
アプリ側には「成功かどうか」と「データ or エラー」を返すだけにします。

# api_client.py

import requests

def request_json(method, url, params=None, json_body=None, headers=None, timeout=5.0):
    try:
        response = requests.request(
            method=method,
            url=url,
            params=params,
            json=json_body,
            headers=headers,
            timeout=timeout,
        )
    except requests.exceptions.Timeout:
        return {
            "ok": False,
            "error_type": "timeout",
            "status_code": None,
            "message": "タイムアウトしました。サーバーの応答がありません。",
        }
    except requests.exceptions.RequestException as e:
        return {
            "ok": False,
            "error_type": "network",
            "status_code": None,
            "message": f"通信エラーが発生しました: {e}",
        }

    if not (200 <= response.status_code < 300):
        return {
            "ok": False,
            "error_type": "http",
            "status_code": response.status_code,
            "message": f"サーバーからエラー応答が返されました。(ステータスコード: {response.status_code})",
        }

    try:
        data = response.json()
    except ValueError:
        return {
            "ok": False,
            "error_type": "json",
            "status_code": response.status_code,
            "message": "レスポンスをJSONとして解釈できませんでした。",
        }

    return {
        "ok": True,
        "error_type": None,
        "status_code": response.status_code,
        "data": data,
    }
Python

ここでの重要ポイントは三つです。

例外(タイムアウト・通信エラー)を外に投げず、ここで吸収していること。
HTTPステータスコードが 200〜299 以外なら「エラー」として扱っていること。
成功時と失敗時を ok フラグと data / message で表現していること。

これで、アプリ側は try / except を書かなくても、
result["ok"] を見るだけで成功かどうか判断できます。


サービス層で「このアプリが使うAPI」をまとめる

BASE_URL とエンドポイントをここに集約する

次に、「このアプリで使うAPI」をまとめる層を作ります。
ここでは、どのURLを叩くか、どのデータを返すかだけを担当します。

# api_service.py

from api_client import request_json

BASE_URL = "https://jsonplaceholder.typicode.com"

def build_url(path: str) -> str:
    return f"{BASE_URL}{path}"

def fetch_users():
    url = build_url("/users")
    result = request_json("GET", url)

    if not result["ok"]:
        return None, result["message"]

    return result["data"], None

def fetch_todos_by_user(user_id: int):
    url = build_url("/todos")
    params = {"userId": user_id}
    result = request_json("GET", url, params=params)

    if not result["ok"]:
        return None, result["message"]

    return result["data"], None

def fetch_posts_by_user(user_id: int):
    url = build_url("/posts")
    params = {"userId": user_id}
    result = request_json("GET", url, params=params)

    if not result["ok"]:
        return None, result["message"]

    return result["data"], None
Python

ここでの重要ポイントは、

BASE_URL を一か所にまとめていること。
エンドポイントごとに「URL+パラメータ+戻り値の形」を決めていること。
アプリ側には「データ or None」と「エラーメッセージ or None」を返していること。

アプリ層は、
「外部APIのURL」や「HTTPメソッド」を知らなくてよくなります。


表示用の整形ロジックを分ける

生のJSONをそのまま print しない

APIから返ってくるデータは、
そのままだと情報が多すぎたり、見づらかったりします。

そこで、「人間に見せる形」に整形する関数を用意します。

# formatters.py

def format_users(users: list) -> str:
    lines = []
    lines.append(f"ユーザー数: {len(users)}")
    lines.append("-----")
    for user in users:
        line = f"[{user['id']}] {user['name']} ({user['email']})"
        lines.append(line)
    return "\n".join(lines)

def format_todos(todos: list, user_id: int) -> str:
    lines = []
    lines.append(f"userId={user_id} の TODO は {len(todos)} 件")
    lines.append("-----")
    for todo in todos:
        status = "✔" if todo["completed"] else "…"
        line = f"{status} [{todo['id']}] {todo['title']}"
        lines.append(line)
    return "\n".join(lines)

def format_posts(posts: list, user_id: int, limit: int = 5) -> str:
    lines = []
    lines.append(f"userId={user_id} の記事は {len(posts)} 件")
    lines.append(f"最初の {min(limit, len(posts))} 件を表示")
    lines.append("-----")
    for post in posts[:limit]:
        lines.append(f"[{post['id']}] {post['title']}")
        body_preview = post["body"].replace("\n", " ")
        lines.append(f"  {body_preview[:60]}...")
        lines.append("-----")
    return "\n".join(lines)
Python

ここでの本質は、

「APIの生データ」と「ユーザーに見せる文字列」を分けていること。

この整形関数は、
コンソールでもGUIでも、そのまま再利用できます。


アプリ層:メニュー付きダッシュボードを作る

ユーザーと対話する「顔」の部分

最後に、ユーザーと対話するメインスクリプトを書きます。
ここでは、サービス層とフォーマッタを呼び出すだけに集中します。

# app_dashboard.py

from api_service import fetch_users, fetch_todos_by_user, fetch_posts_by_user
from formatters import format_users, format_todos, format_posts

def input_user_id() -> int | None:
    user_id_str = input("ユーザーIDを入力してください: ").strip()
    if not user_id_str.isdigit():
        print("数字を入力してください。")
        return None
    return int(user_id_str)

def show_users():
    users, error = fetch_users()
    if error is not None or users is None:
        print("ユーザー一覧の取得に失敗しました。")
        print("詳細:", error)
        return
    text = format_users(users)
    print()
    print(text)

def show_todos_by_user():
    user_id = input_user_id()
    if user_id is None:
        return

    todos, error = fetch_todos_by_user(user_id)
    if error is not None or todos is None:
        print("TODO の取得に失敗しました。")
        print("詳細:", error)
        return

    text = format_todos(todos, user_id)
    print()
    print(text)

def show_posts_by_user():
    user_id = input_user_id()
    if user_id is None:
        return

    posts, error = fetch_posts_by_user(user_id)
    if error is not None or posts is None:
        print("記事の取得に失敗しました。")
        print("詳細:", error)
        return

    text = format_posts(posts, user_id)
    print()
    print(text)

def main():
    while True:
        print()
        print("=== JSONPlaceholder ダッシュボード ===")
        print("1: ユーザー一覧を表示")
        print("2: ユーザーIDから TODO 一覧を表示")
        print("3: ユーザーIDから 記事一覧を表示")
        print("0: 終了")
        choice = input("番号を選んでください: ").strip()

        if choice == "1":
            show_users()
        elif choice == "2":
            show_todos_by_user()
        elif choice == "3":
            show_posts_by_user()
        elif choice == "0":
            print("終了します。")
            break
        else:
            print("不正な入力です。")

if __name__ == "__main__":
    main()
Python

ここでの重要ポイントは三つです。

アプリ層は、requests を一切知らないこと。
「何をしたいか」に名前を付けた関数(show_users など)だけで構成されていること。
エラーが起きても、サービス層から返ってきたメッセージを表示するだけで済んでいること。


7日目で絶対に押さえてほしい本質

「APIアプリを“層”で考えると、一気に楽になる」

今日いちばん大事なのは、
頭の中の構造がこうなっていることです。

外部APIと話すのは api_client(request_json)だけ。
どのエンドポイントをどう使うかは api_service が決める。
ユーザーとのやりとりと表示は app_dashboard と formatter が担当する。

この分け方ができると、

APIが増えても、サービス層を足すだけで済む
表示方法を変えたくなったら、フォーマッタだけを直せばいい
GUIにしたくなったら、「アプリ層」を tkinter に置き換えればいい

という、拡張しやすい形になります。

ここまで来たあなたは、
もう「requests を知っている人」ではなく、
「外部APIを設計してアプリに組み込める人」 です。

あとは、
自分が本当に使いたいAPI(天気、ニュース、翻訳、地図など)を一つ選んで、
今日の構造をそのまま当てはめてみてください。
それをやった瞬間に、「学習用のコード」から「自分のためのツール」に変わります。
そこから先が、いちばん楽しいゾーンです。

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