基礎から学ぶPython入門 90日コース | データベース・Web API - Day 72:API設計

Python 90日で身につけるPython
スポンサーリンク
スポンサーリンク

Day 72:API設計 ― 「使う人の目線で、APIの“形”を整える」

Day 72では、コードを書く前のとても大事なテーマ、 API設計 をじっくり見ていきます。

キーワードはこの4つです。

  • エンドポイント
  • HTTPメソッド
  • ステータスコード
  • エラーレスポンス

ここまで、FastAPIでCRUD APIを作り、SQLiteとつないで動かしてきました。 今日は一歩引いて、「APIの形やルールをどう決めるか」 という視点から整理していきます。

API設計は、 「自分が書きやすいか」だけではなく、 「使う人にとって分かりやすいか・予測しやすいか」 がとても重要です。

エンドポイント ― 「APIの住所をどう決めるか」

エンドポイントって何?

エンドポイント(Endpoint) は、 ざっくり言うと 「APIの住所」 です。

  • https://api.example.com/users
  • https://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
Python

201 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
Python

400 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")
    ...
Python

404 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
Python

500 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
    }
  }
}
JSON
  • code:機械的に扱いやすいエラーコード
  • 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")
Python

Day 72のまとめ ― 「APIの“形”を整えることは、相手への思いやりでもある」

今日の主役は、

  • エンドポイント:APIの住所をどう決めるか
  • HTTPメソッド:どんな操作をしたいのかを表す言葉
  • ステータスコード:結果を数字で伝える仕組み
  • エラーレスポンス:失敗したときに、何がどうダメだったのかを伝えるメッセージ

でした。

API設計は、 ただ「動けばいい」ではなく、 「使う人が迷わないように」「予測しやすいように」「失敗したときも理由が分かるように」 形を整えていく作業です。

コードを書く前に、 エンドポイントの一覧を紙に書き出してみたり、 「この場面ではどのステータスコードがふさわしいかな」と考えてみたりすることは、 地味に見えて、実はとても大きな差を生みます。

Day 72でその感覚をつかんだことで、 この先のAPI開発が、 ただの「動くコード」から、 「人にとって使いやすいサービス」 へと、一段階深まっていきます。

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