
STEP 4では、これまで学んだGET・POSTに加えて、PUT・DELETEを使ったCRUD操作を学びます。
データの取得・登録・更新・削除というWeb APIの基本的な処理を一通り実装します。
また、HTTPExceptionを使ったエラー処理や、HTTPステータスコードの指定方法についても確認します。
最後にCRUD APIを自力で実装し、今回学んだ内容を理解できているか確認します。

それではSTEP4を始めましょう!
今回学ぶこと
今回のステップで学ぶ内容を一覧で出してもらいました。

STEP4では以下の内容を学習します。
- CRUDとHTTPメソッドの対応を整理する
- CRUD確認用の簡単なデータを用意する
- GETでデータを取得する処理を作る
- PUTでデータを更新する
- DELETEでデータを削除する
- HTTPExceptionを使ってエラーを返す
- HTTPステータスコードを指定する
- CRUD全体を通して動作確認する
- 自力課題
学習開始
それでは学習をはじめます。
CRUDとHTTPメソッドの対応を整理する

CRUDは、データを扱うアプリケーションで基本となる4つの操作の頭文字です。
| CRUD | 意味 | HTTPメソッド | 例 |
|---|---|---|---|
| Create | 新規作成 | POST | 商品を登録する |
| Read | 取得 | GET | 商品を取得する |
| Update | 更新 | PUT | 商品情報を更新する |
| Delete | 削除 | DELETE | 商品を削除する |
CRUD確認用の簡単なデータを用意する

今回は商品データを題材にします。
実際のWeb APIならデータはDBに保存しますが、今はCRUDとFastAPIの書き方に集中したいので、Pythonの辞書を簡易的なデータ保存先として使います。
まず、app/main.py に次のデータを追加してください。
追加するコード↓
# CRUDの動作確認用データ
products = {
1: {
"name": "ノートPC",
"price": 120000,
"description": "開発用PC",
},
2: {
"name": "マウス",
"price": 3000,
"description": None,
},
}

これから作るAPIでは、この辞書に対して次のような操作を行います。
GET → productsから取得
POST → productsへ追加
PUT → productsを書き換え
DELETE → productsから削除
GETでデータを取得する処理を作る

app/main.py に次のAPIを追加してください。
追加するコード↓
@app.get("/products/{product_id}")
def get_product(product_id: int):
# 指定された商品IDのデータを取得する
return products.get(product_id)

今回新しく注目したいのはproducts.get(product_id)です。
たとえば、GET /products/1とリクエストすると、products.get(1)が実行され、先ほど用意した次の商品が返ります。
{
"name": "ノートPC",
"price": 120000,
"description": "開発用PC"
}

Swagger UIから、GET /products/1のリクエストを実行してください。上記の内容がレスポンスで返ってくるはずです。
実行結果↓


続いて、GET /products/2も試してください。以下のレスポンスが返ってくればOKです。
{
"name": "マウス",
"price": 3000,
"description": null
}
実行結果↓

PUTでデータを更新する

まず、更新用のPydanticモデルを用意します。
STEP 3で作成した ProductCreate がすでにある場合は、それとは別に次を追加してください。
追加するコード↓
class ProductUpdate(BaseModel):
name: str
price: int = Field(gt=0)
description: str | None = None

今回はPUTの基本を理解するため、name と price は必須にしています。
次に、更新APIを追加します。
追加するコード↓
@app.put("/products/{product_id}")
def update_product(product_id: int, product: ProductUpdate):
# 指定された商品IDのデータを更新する
products[product_id] = product.model_dump()
return products[product_id]

ここで重要なのは、引数の役割です。
product_id: intはURLの/products/{product_id}に対応するため、パスパラメータです。一方、product: ProductUpdateはPydanticモデルなので、FastAPIはこれをリクエストボディとして扱います。
つまり、PUT /products/1に対して以下のデータを送ると、products[1]の中身がこのデータに置き換わります。
{
"name": "ゲーミングノートPC",
"price": 150000,
"description": "開発・ゲーム用PC"
}

ここもFastAPIというよりPydantic側のポイントです。
product.model_dump()
product は ProductUpdate オブジェクトなので、そのままでは今回の products に入っている普通の辞書とは形式が異なります。
model_dump() を使うことで、
ProductUpdate(
name="ゲーミングノートPC",
price=150000,
description="開発・ゲーム用PC",
)
を、
{
"name": "ゲーミングノートPC",
"price": 150000,
"description": "開発・ゲーム用PC",
}
というPythonの辞書に変換できます。

Swagger UIで、PUT /products/1を選び、リクエストボディを例えば次のようにしてください。
{
"name": "ゲーミングノートPC",
"price": 150000,
"description": "開発・ゲーム用PC"
}
実行して、同じ内容でレスポンスが返ってくればOKです。
実行結果↓


さらに先ほど作った、GET /products/1をもう一度実行してください。
PUTで更新した内容がGETでも返ってくれば、メモリ上の products が実際に更新されたことまで確認できます。
実行結果↓


なお、このコードにはまだ1つ問題があります。
PUT /products/999のような存在しないIDを指定すると、新しい商品として登録されてしまいます。
これは本来の今回のAPIでは望ましくないので、後ほど HTTPException を学ぶ項目で修正します。
DELETEでデータを削除する

app/main.py に、次のAPIを追加してください。
追加するコード↓
@app.delete("/products/{product_id}")
def delete_product(product_id: int):
# 指定された商品IDのデータを削除する
deleted_product = products.pop(product_id)
return deleted_product

今回のポイントはproducts.pop(product_id)です。
Pythonの辞書で pop() を使うと、
- 指定したキーのデータを辞書から削除する
- 削除した値を戻り値として返す
という2つのことを同時に行えます。
たとえば、products.pop(2)を実行すると、商品ID2のデータがproductsから削除され、その削除された商品データがdeleted_productに入ります。

Swagger UIから、まずDELETE /products/2を実行してください。
次のレスポンスが返れば削除処理自体は成功しています。
{
"name": "マウス",
"price": 3000,
"description": null
}
実行結果↓


もう一度、GET /products/2を実行してみてください。
商品2が削除されていれば null が返るはずです。
実行結果↓


ここでも、あえてまだ問題を残しています。
DELETE /products/999のように存在しないIDを指定すると、products.pop(product_id) で KeyError が発生します。
これも、後ほど HTTPException を学ぶ項目で修正します。
HTTPExceptionを使ってエラーを返す

FastAPIでは、HTTPエラーを返したいときに HTTPException を使います。
まず、app/main.py のimportを修正してください。
修正コード↓
from fastapi import FastAPI, HTTPException

HTTPException を追加したら、まずGETを次のように修正します。
修正コード↓
@app.get("/products/{product_id}")
def get_product(product_id: int):
# 指定された商品IDの商品を取得する
product = products.get(product_id)
# 商品が存在しない場合は404エラーを返す
if product is None:
raise HTTPException(
status_code=404,
detail="Product not found",
)
return product
ここで重要なのが以下です。
raise HTTPException(
status_code=404,
detail="Product not found",
)
普通のPythonでは、例外は「予期しない問題が起きた」ときに使うイメージが強いですが、FastAPIでは HTTPException を使って、このリクエストに対して、このHTTPエラーを返すという処理を明示できます。

現在のPUTは、存在しないIDでも新しくデータを作れてしまいました。
それを防ぐため、更新前に存在チェックを追加します。
修正コード↓
@app.put("/products/{product_id}")
def update_product(product_id: int, product: ProductUpdate):
# 商品が存在しない場合は404エラーを返す
if product_id not in products:
raise HTTPException(
status_code=404,
detail="Product not found",
)
# 指定された商品IDのデータを更新する
products[product_id] = product.model_dump()
return products[product_id]
これで、PUT /products/999を実行しても、新しい商品は作られません。

DELETEも同じ考え方です。
修正コード↓
@app.delete("/products/{product_id}")
def delete_product(product_id: int):
# 商品が存在しない場合は404エラーを返す
if product_id not in products:
raise HTTPException(
status_code=404,
detail="Product not found",
)
# 指定された商品IDのデータを削除する
deleted_product = products.pop(product_id)
return deleted_product
これで、存在しないIDを指定しても KeyError ではなく、APIとして意味のある404エラーになります。

Swagger UIで、次の3つを試してください。
GET /products/999
PUT /products/999
DELETE /products/999
いずれもレスポンスのHTTPステータスが、404となり、以下のレスポンスが返ってくればOKです。
{
"detail": "Product not found"
}
GET /products/999の実行結果↓

PUT /products/999の実行結果↓

DELETE /products/999の実行結果↓


ここでの重要なポイントは、次のような考え方です。
存在しないデータ
↓
Python内部のエラーとして落とすのではなく
↓
API利用者に意味のあるHTTPエラーとして返す
DRFでいう NotFound や、get_object_or_404() などに近い役割ですね。
404をAPIドキュメントに反映させるには?
該当データが存在しない場合に404を返すように修正しましたが、Swagger UIのAPIドキュメントには404のレスポンスが反映されていませんでした。
そこでChatGPT先生にAPIドキュメントに404を反映させる方法を聞きました。

HTTPException で実際に 404 を返す処理を書いても、Swagger UI(OpenAPIドキュメント)にはその404レスポンスは自動では追加されません。
Swagger UIに「このAPIは404を返す可能性があります」と記載したい場合は、デコレーターの responses に明示します。
@app.get(
"/products/{product_id}",
responses={
404: {
"detail": "Product not found",
},
},
)
上記のように、デコレータに404を追記したらSwagger UIのAPIドキュメントにも404が表示されました。

だた、これだとデコレータ部分のコード量がかなり多くなりそうだと感じたので、実際どうなのか聞いてみました。

はい、その感覚はかなり合っています。FastAPIでAPIドキュメントを丁寧に書こうとすると、デコレータ側の情報量は増えやすいです。
ただし、毎回すべてをベタ書きするわけではありません。実務ではいくつか整理する方法があります。これはちょうど次の STEP 5「Router・Dependsと構成整理」 で触れる部分です。
とのことでした。実際にはデコレータを整理する方法の例をいくつか出してもらったのですが、次のSTEPで触れるとのことなので、ここでは省略します。
HTTPステータスコードを指定する

これまで作ったAPIは、特に指定しなければ正常終了時に基本的に200 OKを返します。
ただし、処理の内容によって適切なステータスコードは異なります。代表的なものは次のとおりです。
| ステータス | 意味 | 今回の例 |
|---|---|---|
200 OK | 正常終了 | GET、PUT |
201 Created | 新しいデータを作成した | POST |
204 No Content | 正常終了したがレスポンスボディなし | DELETE |
404 Not Found | 対象が存在しない | 前の項目で実装済み |
422 Unprocessable Content | バリデーションエラー | Pydanticで経験済み |

FastAPIでは数値を直接書くこともできますが、今回は意味が分かりやすいように status を使います。
まずimportを変更します。
修正コード↓
from fastapi import FastAPI, HTTPException, status

POSTを 201 Created にします。
STEP 3で作成したPOST APIのデコレーターを次のように修正します。
修正コード↓
@app.post(
"/products",
status_code=status.HTTP_201_CREATED,
)
def create_product(product: ProductCreate):
# 商品登録処理
...

status_code=status.HTTP_201_CREATEDとすることで処理が正常に完了すると、201 Createdが返るようになります。
DELETEは今回は200のままにします。DELETEでは 204 No Content が使われることも多いのですが、現在のAPIは、return deleted_productとして削除した商品をレスポンスとして返しています。
204 No Content は名前どおり、レスポンスボディを返さない場合に使うステータスです。そのため、今回はあえてそのままにしておきます。
DELETEだから必ず204というわけではなく、APIのレスポンス設計に合わせてステータスコードを選ぶのがポイントです。
それでは、Swagger UIから既存のPOST APIを実行してください。ステータスが201になればOKです。
実行結果↓


今回のポイントは、次のように整理すると分かりやすいです。
HTTPException:異常系のステータスコードを指定status_code:正常系のステータスコードを指定
CRUD全体を通して動作確認する

ここでは新しい実装は増やさず、これまで作ったAPIを Create → Read → Update → Delete の順番で実際に操作して、CRUDとして一連の流れが成立しているか確認します。
今回は、1つの商品を登録してから削除するところまで順番に試してみましょう。
一連の流れとしては、次のようになります。
POST
商品を作成
↓
GET
作成した商品を取得
↓
PUT
商品を更新
↓
GET
更新されたことを確認
↓
DELETE
商品を削除
↓
GET
404になることを確認
Create:商品を登録する

Swagger UIから、POST /productsを実行します。
例えば次の商品を登録してください。
{
"name": "キーボード",
"price": 8000,
"description": "開発用キーボード"
}
Status Code: 201になっていることを確認してください。POST実行後に、その商品のIDを使って、GET /products/{product_id}を実行してみてください。
ここでPOSTした商品をGETできれば、Create → Read がつながっています。
ChatGPT先生は「POSTで登録した商品をGETリクエストで取得しろ」と言っていますが、現状のPOSTの処理はリクエストボディをそのまま返すだけの処理になっているため、追加も何もしていません。
# 現在のPOSTの処理
@app.post(
"/products",
status_code=status.HTTP_201_CREATED,
)
def create_product(product: ProductCreate):
# リクエストボディから受け取った商品データをそのまま返す
return product
その旨を伝えた所、以下のようにPOSTを修正しろと指示がありました。
@app.post(
"/products",
status_code=status.HTTP_201_CREATED,
)
def create_product(product: ProductCreate):
# 現在の商品IDの最大値から、新しい商品IDを作成する
new_product_id = max(products.keys(), default=0) + 1
# Pydanticモデルを辞書に変換する
product_data = product.model_dump()
# 新しい商品を保存する
products[new_product_id] = product_data
# 作成された商品IDと商品データを返す
return {
"id": new_product_id,
**product_data,
}

new_product_id = max(products.keys(), default=0) + 1
この処理は今回のメモリ上の簡易データ用です。実際にDBを使うSTEP 6では、IDの採番は通常DB側に任せるので、この処理を自分で書く必要はなくなります。
今回はこちらの進め方に抜けがありました。STEP 3のPOSTをそのままCRUDのCreateとして扱ってしまっていました。
まずこの修正を入れて、POSTした商品のレスポンスに id が含まれ、そのIDを使ったGETで同じ商品を取得できるか確認してください。
言われた通り処理を修正してPOST /productsを実行。
実行結果↓

Status Code:201で修正後コードのレスポンス形式で返ってきていることが確認できました。
GET /products/3でPOSTとしたデータを取得した結果↓

問題なく取得できました。
Update:登録した商品を更新する

同じIDに対して、PUT /products/{product_id}を実行し、次のように変更します。
{
"name": "メカニカルキーボード",
"price": 12000,
"description": "開発用メカニカルキーボード"
}
その後、GET /products/{product_id}を実行し、更新後の内容が返ってくることを確認します。
先ほど登録したデータのproduct_idは「3」なので、{product_id}を3に置き換えて実行していきます。
PUT /products/3の実行結果↓

GET /products/3の実行結果↓

PUTでデータが更新され、GETで更新されたデータを取得することができました。
Delete:商品を削除する

最後に、DELETE /products/{product_id}で同じ商品を削除します。
その後もう一度、GET /products/{product_id}を実行します。
今度は、Status Code: 404で、以下のようになればOKです。
{
"detail": "Product not found"
}
DELETE /products/3の実行結果↓

GET /products/3の実行結果↓

想定通りの結果になりました。
自力課題

今回は、ここまでの商品APIを参考にしながら、別のリソースのCRUD APIを自力で作ってみましょう。
課題内容

題材は 書籍(Book)API にします。
メモリ上に書籍を保存する books 辞書を用意し、次のAPIを実装してください。
| HTTPメソッド | パス | 処理 |
|---|---|---|
POST | /books | 書籍を登録する |
GET | /books/{book_id} | 書籍を1件取得する |
PUT | /books/{book_id} | 書籍を更新する |
DELETE | /books/{book_id} | 書籍を削除する |

書籍には、次の項目を持たせてください。
title 書籍名
price 価格
description 説明(任意)
条件は次のとおりです。
titleは文字列・必須priceは整数・必須・0より大きい値descriptionは文字列またはNone・省略可能- POSTでは新しいIDを採番して
booksに保存する - POST成功時は
201 Created - GET / PUT / DELETEで存在しないIDを指定した場合は
404 Not Found - エラー時の
detailは"Book not found"とする - PUTでは
title・price・descriptionをまとめて更新する - DELETE成功時は、削除した書籍データを返す
完了条件

Swagger UIを使って、次の流れがすべて成功すれば課題クリアです。
POST
↓
作成したIDでGET
↓
PUTで内容を変更
↓
GETで変更を確認
↓
DELETE
↓
GETして404を確認
実装結果
実装したコード(実装箇所のみ抜粋)↓
class BookCreate(BaseModel):
title: str
price: int = Field(gt=0)
description: str | None = None
class BookUpdate(BaseModel):
title: str
price: int = Field(gt=0)
description: str | None = None
books = {}
@app.post(
"/books",
status_code=status.HTTP_201_CREATED,
)
def create_book(book: BookCreate):
# 現在の書籍IDの最大値から、新しい書籍IDを作成する
new_book_id = max(books.keys(), default=0) + 1
# Pydanticモデルを辞書に変換する
book_data = book.model_dump()
# 新しい書籍を保存する
books[new_book_id] = book_data
# 作成された書籍IDと書籍データを返す
return {
"id": new_book_id,
**book_data,
}
@app.get("/books/{book_id}")
def get_book(book_id: int):
book = books.get(book_id)
# 書籍が存在しない場合は404エラーを返す
if book is None:
raise HTTPException(
status_code=404,
detail="Book not found",
)
return book
@app.put("/books/{book_id}")
def update_book(book_id: int, book: BookUpdate):
# 書籍が存在しない場合は404エラーを返す
if book_id not in books:
raise HTTPException(
status_code=404,
detail="Book not found",
)
# 指定された書籍IDのデータを更新する
books[book_id] = book.model_dump()
return books[book_id]
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
# 書籍が存在しない場合は404エラーを返す
if book_id not in books:
raise HTTPException(
status_code=404,
detail="Book not found",
)
# 指定された書籍IDのデータを削除する
deleted_book = books.pop(book_id)
return deleted_book
POSTの実行結果↓

作成したIDでGETした結果↓

ちなみに、存在しないIDをGETしたときは404が返るようにしています。

PUTで内容を変更した結果↓

PUTしたデータをGETした結果↓

ちなみに、存在しないIDをPUTしたときは404が返るようにしています。

DELETEした結果↓

DELETEしたIDをGETした結果↓

ちなみに、存在しないIDをDELETEしたときは404が返るようにしています。
ChatGPT先生のレビュー結果

かなりきれいに書けています。要件も満たせています。
コード上で気になる点を挙げるなら1つだけです。
books = {}
この書き方でも動作上は問題ありませんが、型を少し明確にするなら例えば、以下のようにも書けます。
books: dict[int, dict] = {}
ただし、今回の自力課題では必須ではありません。今のままで十分です。
実行結果もすべて問題ありません。自力課題はクリアです。
まとめ
今回はCRUDとエラー処理について学びました。
CRUDとHTTPメソッドについては、既知の内容だったため、いい復習になりました。
FastAPIでは以下のように使い分けることを学びました。
@app.post(...) # 登録
@app.get(...) # 取得
@app.put(...) # 更新
@app.delete(...) # 削除
また、APIの処理中に任意のエラーを返したい場合は、HTTPException を使用するということも学びました。
今回は該当データがない場合にHTTPException で404を返すようにしましたが、他にも相関チェックやAPIの処理中にエラーと判断した場合など、処理に応じたHTTPステータスを返す際にHTTPExceptionを使うのだと理解しました。
今回までの学習で一通りのCRUD操作を実装して、基本的な実装については理解できたかなと感じています。
コメント