Day 72:API設計 ― 「使う人の目線で、APIの“形”を整える」
Day 72では、コードを書く前のとても大事なテーマ、 API設計 をじっくり見ていきます。
キーワードはこの4つです。
- エンドポイント
- HTTPメソッド
- ステータスコード
- エラーレスポンス
ここまで、FastAPIでCRUD APIを作り、SQLiteとつないで動かしてきました。 今日は一歩引いて、「APIの形やルールをどう決めるか」 という視点から整理していきます。
API設計は、 「自分が書きやすいか」だけではなく、 「使う人にとって分かりやすいか・予測しやすいか」 がとても重要です。
エンドポイント ― 「APIの住所をどう決めるか」
エンドポイントって何?
エンドポイント(Endpoint) は、 ざっくり言うと 「APIの住所」 です。
https://api.example.com/usershttps://api.example.com/tasks/1
のようなURLが、エンドポイントです。
FastAPIでは、 @app.get("/users") や @app.post("/tasks") のように、 パス部分(/users や /tasks)を決めていきます。
リソースを意識したエンドポイント設計
API設計では、「リソース」 という考え方がよく使われます。
users:ユーザーというリソースtasks:タスクというリソースcustomers:顧客というリソース
リソースごとに、 「一覧」「1件」「作成」「更新」「削除」 をどう表現するかを決めていきます。
よくあるパターンはこんな感じです。
- 一覧:
GET /users - 作成:
POST /users - 1件取得:
GET /users/{id} - 更新:
PUT /users/{id} - 削除:
DELETE /users/{id}
このように、 「エンドポイントはリソースを表す」「操作はHTTPメソッドで表す」 というスタイルが、REST風のAPI設計の基本になります。
例:タスク管理APIのエンドポイント一覧
GET /tasks # タスク一覧を取得
POST /tasks # 新しいタスクを作成
GET /tasks/{id} # 指定IDのタスクを取得
PUT /tasks/{id} # 指定IDのタスクを更新
DELETE /tasks/{id} # 指定IDのタスクを削除
このように並べてみると、 「何がどこにあるのか」がぐっと見通しよくなります。
HTTPメソッド ― 「どんな操作をしたいのか」を表す言葉
よく使うメソッドを整理する
API設計でよく登場するHTTPメソッドは、主にこの4つです。
- GET:情報を取得する(読む)
- POST:新しい情報を作る(送る)
- PUT:既存の情報を更新する(上書きする)
- DELETE:情報を削除する
それぞれの役割を、タスク管理APIを例にして見てみましょう。
GET ― 読む
@app.get("/tasks")
def list_tasks():
"""
タスク一覧を取得するGET APIです。
"""
...
Python- 「何かを作る」のではなく、「今あるものを知りたい」ときに使うメソッドです。
POST ― 作る
@app.post("/tasks")
def create_task(task_in: TaskCreate):
"""
新しいタスクを作成するPOST APIです。
"""
...
Python- 新しいタスクを登録したいときに使います。
- リクエストボディに、作成したい内容をJSONで送るのが一般的です。
PUT ― 上書きする
@app.put("/tasks/{task_id}")
def update_task(task_id: int, task_in: TaskUpdate):
"""
既存のタスクを更新するPUT APIです。
"""
...
Python- 「このタスクを、こういう内容に差し替えたい」というときに使います。
- 全体を上書きするイメージが強いメソッドです。
DELETE ― 消す
@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
"""
既存のタスクを削除するDELETE APIです。
"""
...
Python- 「もういらないので消したい」というときに使います。
メソッドと意味を揃えることが大事
API設計では、
- 「作るのにGETを使う」
- 「削除なのにPOSTを使う」
といったことは、基本的には避けたほうがよいです。
理由はシンプルで、
- 他の開発者が「メソッドを見て、何をするAPIか予測しやすい」
- ライブラリやツールも、メソッドに応じた扱いをしてくれる
からです。
「メソッドの意味と、APIの動きが一致しているか」 という視点は、API設計のとても大事なポイントです。
ステータスコード ― 「結果を数字で伝える」
ステータスコードって何?
HTTPレスポンスには、 ステータスコード という3桁の数字が必ず含まれています。
200 OK:成功201 Created:新規作成成功400 Bad Request:リクエストがおかしい404 Not Found:対象が見つからない500 Internal Server Error:サーバー側のエラー
などが代表的です。
API設計では、 「どんな結果のときに、どのステータスコードを返すか」 を決めておくことが重要です。
よく使うステータスコードと場面
200 OK ― 成功の基本形
- GETで正常にデータを返せたとき
- PUTやDELETEで問題なく処理できたとき
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
task = find_task(task_id)
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
return task # デフォルトで 200 OK
Python201 Created ― 新規作成成功
- POSTで新しいリソースを作成したときに使うと、より丁寧です。
FastAPIでは、status_code を指定できます。
from fastapi import status
@app.post("/tasks", status_code=status.HTTP_201_CREATED)
def create_task(task_in: TaskCreate):
task = save_task(task_in)
return task
Python400 Bad Request ― リクエストがおかしい
- 必須項目が欠けている
- フォーマットが不正
- バリデーションエラー
など、「クライアント側の入力がおかしい」場合に使います。
from fastapi import HTTPException
@app.post("/users")
def create_user(user_in: UserCreate):
if len(user_in.name) == 0:
raise HTTPException(status_code=400, detail="Name must not be empty")
...
Python404 Not Found ― 見つからない
- 指定されたIDのリソースが存在しない
- URLが間違っている
など、「対象が見つからない」場合に使います。
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
task = find_task(task_id)
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
return task
Python500 Internal Server Error ― サーバー側の問題
- 予期しない例外
- DB接続エラー
- バグ
など、「サーバー側の問題」のときに使われます。
FastAPIでは、予期しない例外が起きた場合、 自動的に500が返されます。
ステータスコードは「APIの表情」
ステータスコードは、 「APIが今どんな状態なのか」を、数字で表現するもの です。
- 200台:うまくいった
- 400台:クライアント側の問題
- 500台:サーバー側の問題
というざっくりした分類を覚えておくだけでも、 API設計の見通しがかなり良くなります。
エラーレスポンス ― 「失敗したときこそ、丁寧に伝える」
エラー時に何を返すべきか
APIは、いつも成功するわけではありません。
- 入力がおかしい
- 対象が存在しない
- サーバー側で問題が起きた
など、いろいろな理由で失敗します。
そのときに、
「ただステータスコードだけ返す」のか 「エラー内容をJSONで分かりやすく返す」のか
で、APIの使いやすさが大きく変わります。
シンプルなエラーレスポンスの形
よくあるパターンは、こんな形です。
{
"error": {
"code": "TASK_NOT_FOUND",
"message": "指定されたタスクは存在しません。",
"detail": {
"task_id": 123
}
}
}
JSONcode:機械的に扱いやすいエラーコードmessage:人間向けの説明detail:追加情報(任意)
初心者向けの段階では、 まずは message だけでも十分です。
FastAPIでのエラーレスポンス
FastAPIでは、HTTPException を使うことで、 ステータスコードとメッセージを簡単に返せます。
from fastapi import HTTPException
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
task = find_task(task_id)
if task is None:
# 404 Not Found とメッセージを返します。
raise HTTPException(status_code=404, detail="Task not found")
return task
Pythonレスポンスはこんな形になります。
{
"detail": "Task not found"
}
JSONもう少し構造化したエラーを返したい場合は、 自分でレスポンスを作ることもできます。
from fastapi import Response, status
from fastapi.responses import JSONResponse
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
task = find_task(task_id)
if task is None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={
"error": {
"code": "TASK_NOT_FOUND",
"message": "指定されたタスクは存在しません。",
"detail": {"task_id": task_id},
}
},
)
return task
Pythonエラー設計のポイント
- ステータスコードと内容を揃える
- 404なら「見つからない」系のメッセージ
- 400なら「入力がおかしい」系のメッセージ
- 人間が読んで意味が分かるメッセージにする
- 機械的に扱えるコードを用意しておくと、クライアント側が楽になる
失敗したときこそ、 「何がどうダメだったのか」を丁寧に伝えるAPI は、 使う人にとってとてもありがたい存在になります。
Day 72ミニテンプレート ― API設計の基本を反映したサンプル
最後に、今日の内容を反映した タスク管理APIの設計サンプル を載せておきます。
# day72_api_design_template.py
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
app = FastAPI()
class TaskBase(BaseModel):
title: str
description: str | None = None
done: bool = False
class TaskCreate(TaskBase):
pass
class TaskRead(TaskBase):
id: int
tasks: list[TaskRead] = []
next_id: int = 1
@app.get("/", status_code=status.HTTP_200_OK)
def read_root():
"""
APIのトップ。簡単な説明を返します。
"""
return {"message": "Day 72: API設計サンプル", "version": "1.0.0"}
@app.get("/tasks", response_model=list[TaskRead], status_code=status.HTTP_200_OK)
def list_tasks():
"""
タスク一覧を取得するGET APIです。
"""
return tasks
@app.get("/tasks/{task_id}", response_model=TaskRead, status_code=status.HTTP_200_OK)
def get_task(task_id: int):
"""
指定IDのタスクを取得するGET APIです。
見つからない場合は 404 を返します。
"""
for task in tasks:
if task.id == task_id:
return task
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
@app.post("/tasks", response_model=TaskRead, status_code=status.HTTP_201_CREATED)
def create_task(task_in: TaskCreate):
"""
新しいタスクを作成するPOST APIです。
必須項目が欠けている場合は 400 を返します。
"""
global next_id
if len(task_in.title.strip()) == 0:
# タイトルが空の場合は 400 Bad Request
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Title must not be empty")
task = TaskRead(
id=next_id,
title=task_in.title,
description=task_in.description,
done=task_in.done,
)
tasks.append(task)
next_id += 1
return task
@app.put("/tasks/{task_id}", response_model=TaskRead, status_code=status.HTTP_200_OK)
def update_task(task_id: int, task_in: TaskCreate):
"""
既存のタスクを更新するPUT APIです。
見つからない場合は 404、タイトルが空なら 400 を返します。
"""
if len(task_in.title.strip()) == 0:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Title must not be empty")
for index, task in enumerate(tasks):
if task.id == task_id:
updated = TaskRead(
id=task_id,
title=task_in.title,
description=task_in.description,
done=task_in.done,
)
tasks[index] = updated
return updated
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
@app.delete("/tasks/{task_id}", status_code=status.HTTP_200_OK)
def delete_task(task_id: int):
"""
既存のタスクを削除するDELETE APIです。
見つからない場合は 404 を返します。
"""
for index, task in enumerate(tasks):
if task.id == task_id:
tasks.pop(index)
return {"message": f"Task {task_id} deleted"}
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
PythonDay 72のまとめ ― 「APIの“形”を整えることは、相手への思いやりでもある」
今日の主役は、
- エンドポイント:APIの住所をどう決めるか
- HTTPメソッド:どんな操作をしたいのかを表す言葉
- ステータスコード:結果を数字で伝える仕組み
- エラーレスポンス:失敗したときに、何がどうダメだったのかを伝えるメッセージ
でした。
API設計は、 ただ「動けばいい」ではなく、 「使う人が迷わないように」「予測しやすいように」「失敗したときも理由が分かるように」 形を整えていく作業です。
コードを書く前に、 エンドポイントの一覧を紙に書き出してみたり、 「この場面ではどのステータスコードがふさわしいかな」と考えてみたりすることは、 地味に見えて、実はとても大きな差を生みます。
Day 72でその感覚をつかんだことで、 この先のAPI開発が、 ただの「動くコード」から、 「人にとって使いやすいサービス」 へと、一段階深まっていきます。
