5日目のゴール
5日目のテーマは
「“本物のAPI”を意識して、API層とアプリ層をきちんと分けて書く」 ことです。
ここまでであなたはすでに、
URLにアクセスしてJSONを受け取り、エラー処理を入れ、ネストしたJSONも読めるようになりました。
5日目では一歩進んで、
APIを叩くコード(API層)と、
それを使うアプリ側のコード(アプリ層)を分けて設計すること。
そして、「人間に見せるメッセージ」と「開発者向けの情報」を意識して書き分けること。
この二つをテーマにします。
API層とアプリ層を分ける、という考え方
「requests を直接あちこちで呼ばない」発想
小さなスクリプトなら、どこでも requests.get(...) を書いても動きます。
でも、アプリになってくると、それはだんだん「散らかったコード」になります。
そこで大事になるのが、
APIを叩く部分を「専用の関数(あるいはモジュール)」に閉じ込める
アプリ側は「その関数を呼んで結果を使うだけ」にする
という分離です。
イメージとしては、
外部APIと話すのは「通訳(API層)」
アプリ本体は「通訳に“こういう情報が欲しい”と頼むだけ」
という役割分担です。
この分け方ができると、
APIのURLが変わった
タイムアウトを変えたい
ヘッダーを追加したい
といった変更があっても、
API層だけを直せば済むようになります。
まずは「APIクライアントモジュール」を作る
request_json を“外部API専用の窓口”にする
5日目では、request_json を「API層の入口」として使います。
ここに、requests とエラー処理を全部閉じ込めます。
# 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ここでの重要ポイントを深掘りします。
この関数は「例外を外に投げない」ようにしています。
代わりに、成功かどうかを ok というフラグで返しています。
失敗した場合は、error_type と message に情報を詰めています。
つまり、アプリ側は try / except を書かなくても、
戻り値の ok を見て分岐するだけでよくなる
という設計です。
これが「API層でエラーを吸収して、アプリ層には“結果”として渡す」という考え方です。
アプリ層から見たときの「きれいな顔」を作る
「このアプリで何がしたいか」に名前を付ける
例えば、「ユーザー一覧を取得して表示したい」アプリを考えます。
アプリ層から見たときに、こんな関数があると気持ちいいです。
users = fetch_users()
Pythonあるいは、
todo = fetch_todo(todo_id)
Pythonここで大事なのは、
アプリ層は「外部APIのURL」を知らなくていい、ということです。
URLやパラメータは、API層の中に閉じ込めます。
# api_service.py
from api_client import request_json
BASE_URL = "https://jsonplaceholder.typicode.com"
def fetch_users():
url = f"{BASE_URL}/users"
result = request_json("GET", url)
if not result["ok"]:
return None, result["message"]
return result["data"], None
def fetch_todo(todo_id: int):
url = f"{BASE_URL}/todos/{todo_id}"
result = request_json("GET", url)
if not result["ok"]:
return None, result["message"]
return result["data"], None
Pythonここでのポイントは二つです。
一つ目は、BASE_URL を一か所にまとめていること。
二つ目は、「成功したらデータとエラーなし」「失敗したら None とエラーメッセージ」という形で返していることです。
アプリ層から見ると、こうなります。
from api_service import fetch_users
users, error = fetch_users()
if error is not None:
print("ユーザー取得に失敗:", error)
else:
print("ユーザー数:", len(users))
Pythonアプリ側は、
「このアプリで何をしたいか」に集中できていて、
HTTPだのJSONだのは意識しなくて済んでいます。
「人間向けメッセージ」と「開発者向け情報」を分ける
エラーの中身をどう扱うか
request_json の中では、error_type や status_code など、わりと細かい情報を持っています。
でも、アプリのユーザーに見せたいのは、
もっとシンプルなメッセージです。
例えば、
「ネットワークエラーが発生しました。時間をおいて再度お試しください。」
「サーバーからエラー応答が返されました。(ステータスコード: 500)」
などです。
一方で、開発者としては、
ログにはもう少し詳しい情報を残しておきたいこともあります。
5日目では、まずはシンプルに、
ユーザーには message をそのまま見せる
開発者向けには、必要なら error_type や status_code を print する
くらいの分け方で十分です。
from api_service import fetch_todo
def main():
todo_id_str = input("TODO ID を入力してください: ").strip()
if not todo_id_str.isdigit():
print("数字を入力してください。")
return
todo_id = int(todo_id_str)
todo, error = fetch_todo(todo_id)
if error is not None:
print("取得に失敗しました。")
print("詳細:", error) # ユーザー向けメッセージ
return
print("ID:", todo["id"])
print("タイトル:", todo["title"])
print("完了:", todo["completed"])
if __name__ == "__main__":
main()
Pythonここでの本質は、
「エラーの詳細をどこまで誰に見せるか」を意識すること
です。
GUIアプリになったときには、
このメッセージをラベルやダイアログに出すだけで、
「ちゃんとしたアプリ感」が一気に増します。
5日目の例題:ユーザー検索コンソールアプリ
仕様を言葉で整理する
ユーザー一覧APIを叩く。
名前に特定の文字列を含むユーザーだけを抽出する。
抽出結果を整形して表示する。
APIエラーが起きても落ちずにメッセージを出す。
API層とアプリ層を分けて書きます。
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,
}
Pythonapi_service.py(このアプリ専用のAPI層)
from api_client import request_json
BASE_URL = "https://jsonplaceholder.typicode.com"
def fetch_users():
url = f"{BASE_URL}/users"
result = request_json("GET", url)
if not result["ok"]:
return None, result["message"]
return result["data"], None
Pythonapp.py(アプリ層:ユーザー検索ロジック)
from api_service import fetch_users
def search_users_by_name(keyword: str):
users, error = fetch_users()
if error is not None:
return None, error
keyword_lower = keyword.lower()
matched = []
for user in users:
name = user["name"]
if keyword_lower in name.lower():
matched.append(user)
return matched, None
def main():
print("ユーザー検索アプリ(名前に含まれる文字列で検索)")
keyword = input("検索キーワードを入力してください: ").strip()
if keyword == "":
print("キーワードが空です。終了します。")
return
results, error = search_users_by_name(keyword)
if error is not None:
print("検索に失敗しました。")
print("詳細:", error)
return
if not results:
print("該当するユーザーはいませんでした。")
return
print(f"{len(results)} 件ヒットしました。")
for user in results:
print(f"- {user['id']}: {user['name']} ({user['email']})")
if __name__ == "__main__":
main()
Pythonこの三つのファイルで、5日目のテーマが全部入っています。
APIクライアント(requests とエラー処理)は api_client.py に閉じ込められている。
どのURLを叩くか、どのデータを返すかは api_service.py が担当している。
アプリ本体(検索ロジックと入出力)は app.py にだけ書かれている。
「APIキーが必要なAPI」を見据えた設計の話
今日はコードには書かないけれど、考え方だけ
多くの実用的なAPIは、「APIキー」や「トークン」が必要です。
それらはたいてい、ヘッダーかクエリパラメータで送ります。
例えば、ヘッダーならこんな感じです。
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
}
Python5日目で作った request_json は、すでに headers 引数を受け取れる形になっています。
つまり、APIキーが必要なAPIに切り替えても、
api_service.py の中でヘッダーを用意してrequest_json(..., headers=headers) と渡すだけ
で対応できます。
ここでの本質は、
APIキーやURLのような「環境依存の情報」は、
アプリのあちこちに書かず、API層に閉じ込める
という設計の感覚です。
5日目で絶対に押さえてほしい本質
「requests を直接叩く人」から「API層を設計する人」へ
今日いちばん大事なのは、
頭の中の構造がこう変わることです。
今までは、
思いついたところで requests.get(...) を書く
だったのが、これからは、
外部APIと話すのは api_client(共通クライアント)
このアプリで必要なAPIは api_service がまとめる
アプリ本体は「fetch_xxx」を呼んで結果を使うだけ
という三層構造で考えられるようになることです。
技術的なキーワードをまとめると、
request_json で「成功かどうか」と「エラー情報」をまとめて返す
API層では「URL・メソッド・パラメータ・ヘッダー」を決める
アプリ層では「何をしたいか」に名前を付けた関数(fetch_users など)を呼ぶ
ユーザー向けメッセージと開発者向け情報を意識して分ける
ここまで来たあなたは、
もう「APIを叩ける人」ではなく、
「APIをアプリの中にきれいに組み込める人」 になりつつあります。
6日目・7日目では、
このAPI層と、これまで学んだ tkinter のGUIを組み合わせて、
「ボタンを押したらAPIからデータを取ってきて、画面に表示する」
という、いよいよ“中級者らしいAPI取得アプリ”を形にしていきましょう。


