Technology

FastAPIで非同期APIエンドポイントを作る(Pydanticでの型付け・パスパラメータ・エラー応答)

FastAPIは、PythonでWeb APIを作るためのフレームワークです。

FastAPIの大きな特徴は、Pythonの型ヒントをそのまま活用する点です。型ヒントは、変数や関数の引数に「どんな種類のデータか」を書き添える記法のことで、例えば、name: strは「変数nameを文字列とする」という意味になります。FastAPI はこの型ヒントを読み取って、送られてきたデータが正しい形かを自動でチェックし、さらに API の説明ドキュメントまで自動生成します。

もう1つの特徴は、非同期処理を書きやすい点です。API処理では外部に問い合わせを行い、結果を待つ時間が生じます。その待ち時間に別のリクエストを処理することを非同期処理といいます。I/O の待ちが多い API では、非同期処理を行うことで全体の効率が上がります。

本記事では、FastAPIで非同期のエンドポイント(リクエストを受け取る関数)を定義し、「書籍」を扱うAPIを例に、Pydanticでの型付け、パスパラメータ、エラー応答およびドキュメントの自動生成について、順を追って解説します。

前提

本記事は、以下の環境を用いています。

  • Python言語を用いて、fastapiライブラリを使います。
  • Python 3.12で動作確認をしています。

※ この記事の動作確認は2026年6月中頃に実施しています。

目次

事前準備

FastAPI と、開発に使うサーバーをまとめてインストールします。

# pip を使っている場合
pip install "fastapi[standard]"
# uv を使っている場合
uv add "fastapi[standard]"

fastapi[standard][standard]を付けると、FastAPI本体に加えて、Uvicorn(ユビコーン)というサーバーや、後で使うfastapiコマンド(FastAPI CLI)などがまとめて入ります。

Uvicorn はASGI サーバーの一つです。ASGIとは、Pythonの非同期処理(async/await)に対応したWebアプリとサーバーの接続仕様のことです。ASGIはAsynchronous Server Gateway Interfaceの略です。

本記事のコードは Python 3.10 以上を前提とします。

最小アプリと起動

一番小さなFastAPIアプリから作成してみます。main.py というファイルを用意して、次のコードを書きます。

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello, FastAPI"}

ポイントを以下に記述します。

■ app = FastAPI()
アプリ本体を作って、変数appに入れています。以降はこのappにエンドポイントを追加していきます。

■ @app.get("/")
この@で始まる行をデコレータといい、「直下に記述した関数に役割を与える印」です。ここでは「パス/へのGETリクエストが来たら、すぐ下のroot関数で処理する」という意味になります。

関数が返した辞書{"message": "Hello, FastAPI"}は、FastAPIが自動的にJSON形式に変換して、レスポンスとして返します。

用語

パスとは、URLのうちドメインより後ろの部分です。例えば、http://127.0.0.1:8000/booksなら/booksがパスにあたります。GET / POSTHTTP メソッド(リクエストの種類)です。ざっくり、GET は「取得する」とき、POST は「新しく送る・作る」ときに使います。

開発用のサーバーは、次のコマンドで起動します。

fastapi dev main.py
# uv の 場合
uv run fastapi dev main.py

起動すると http://127.0.0.1:8000 で待ち受けます。127.0.0.1 は「自分自身のマシン(localhost)」を指すアドレス、8000ポート番号(同じマシン上で複数のサービスを区別するための番号)です。ブラウザでこの URL を開くと、{"message":"Hello, FastAPI"} と表示されます。fastapi dev は開発用のモードで、コードを保存するたびにサーバーを自動で再起動(オートリロード)してくれます。

fastapi dev main.py は、内部では Uvicorn を呼び出しているだけです。次のコマンドとほぼ同じ意味なので、どちらを使っても構いません。

uvicorn main:app --reload
# uv の場合
uv run uvicorn main:app --reload

main:app は「main.py というファイルの中の app という変数」を指す書き方で、--reload がオートリロードの指定です。

起動したサーバーはCtrl + Cで止めることができます。

補足:async defとdefの使い分け

エンドポイントはasync defではなく、ふつうのdef(同期関数)で書くこともできます。defで書いた場合、FastAPIがそれを別のスレッドで実行してくれるので、イベントループ自体は空いています。このため、defでの処理を実行中でも、他のリクエストを受けることができます。逆に注意が必要なのは、async def の中に、終わるまで待ち続ける処理(ブロッキング処理。たとえば time.sleep や同期版のデータベースアクセスなど)を書いてしまう場合です。こうすると処理が終わるまで他のリクエストを受け付けられなくなります。基本的には、「await で待つ非同期処理があるなら async def、そうでなければ def」が目安です。本記事では、書き方を示すためにasync defで書いています。

Pydanticでリクエストのデータを定義する

POSTリクエストなどで送られてくる本体のデータ(リクエストボディ)は、Pydanticというライブラリで受け取ります。Pydanticを使うと、「データがどんな項目を持ち、それぞれどんな型か」をクラスとして定義できます。そして、送られてきたデータがその通りかを自動でチェックしてくれます。このチェックはプログラムの実行中に行われます。つまり、実際にリクエストが届いたときに、そのデータが定義した型と合っているかを確認します。

使い方はBaseModel というクラスを継承して自分のクラスを作り、それをエンドポイント関数の引数に型として指定するだけです。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Book(BaseModel):
    title: str
    author: str
    year: int | None = None

@app.post("/books")
async def create_book(book: Book):
    return book

BookクラスにBaseModelクラスを継承させ、リクエストのフォーマットとして3つのフィールドとその型を定義しています。例えば、year: int | None = Noneyearという変数が、整数型またはNoneを取り、省略された場合(デフォルト)はNoneであることを意味しています。

@app.postはパス(/books)にPOSTリクエストが来たときに実行される関数を表すデコレータです。その関数create_bookの引数を book: Bookと書くと、FastAPI はリクエストボディのJSONを読み取り、Bookの形に当てはめて検証してから関数に渡してくれます。型が合わない、あるいは必須フィールドが足りない場合は、FastAPIが自動的に422というエラーと、どこが問題かを示すJSONを返します。検証のコードを自分で書く必要はありません。

用語

ここで出てきた 422 のような3桁の番号を HTTP ステータスコード と呼びます。リクエストの結果を表す番号で、200 番台は成功、400 番台は「リクエスト側の問題」、500 番台は「サーバー側の問題」を表します。422 は「リクエストの形式は分かるが、内容が処理できない(型違反など)」という意味です。

たとえば次のような JSONをPOSTすると、FastAPIによる型チェックを通過して、そのままJSON形式でレスポンスとして返されます。

{ "title": "吾輩は猫である", "author": "夏目漱石", "year": 1905 }

また、"title"部分をあえて削除して送ると、HTTPステータスコード422と共に、エラーの詳細を示すJSONがレスポンスとして返されます。

{
  "detail": [
    {
      "type": "missing",
      "loc": [
        "body",
        "title"
      ],
      "msg": "Field required",
      "input": {
        "author": "夏目漱石",
        "year": 1905
      }
    }
  ]
}

GET・POSTエンドポイントとパスパラメータ

「書籍を登録する・一覧を取得する・1件だけ取得する」という小さなAPIを作成します。データはメモリ上の辞書に保存することにします。プログラムを再起動すると消えてしまいますが、本記事の趣旨から問題ないものとします。

レスポンスにも型をつけたいので、BookResponseクラスを用意します。先ほどのBookを継承して、サーバー側で割り振るid(識別番号)のフィールドを足したものです。

class BookResponse(Book):
    id: int

books: dict[int, BookResponse] = {}

books: dict[int, BookResponse]={}は、「キーが intid)、値がBookResponse」である空の辞書を定義しています。これに書籍一覧を格納します。

書籍一覧の取得と書籍の登録を行うAPIの作成

次の2つのAPIを追加します。

@app.get("/books")
async def list_books() -> list[BookResponse]:
    return list(books.values())

@app.post("/books")
async def create_book(book: Book) -> BookResponse:
    new_id = max(books.keys(), default=0) + 1
    new_book = BookResponse(id=new_id, **book.model_dump())
    books[new_id] = new_book
    return new_book

書籍一覧の取得を行うAPI(list_books)と書籍の登録を行うAPI(create_book)です。ポイントは以下の2つです。

1つ目は、戻り値の型注釈です。関数の後ろの-> list[BookResponse]は「BookResponseが入ったリスト」を戻り値として返すという意味ですが、FastAPI はこの戻り値の型を、単なるメモ書きではなく「レスポンスの型」としても使います。

2つ目は、番号の割り振りです。max(books.keys(), default=0) は辞書booksのキーを取得して、その最大値を取得しています。つまり、現在登録されている書籍のidの最大値を求めています。そして、「今ある id の最大値に 1 を足したもの」を新しい id にしています。

BookResponse(id=new_id, **book.model_dump())** は、辞書をキーワード引数として展開する書き方です。model_dump()BaseModelクラスから継承されたメソッドで、定義されたモデル(ここではBookクラスのオブジェクト)を辞書に変換します。これにより、book.model_dump(){"title": ..., "author": ..., "year": ...} という辞書を返すので、この行は実質的に BookResponse(id=new_id, title=..., author=..., year=...) と同じ意味になります。受け取った Book の中身に id を足して、保存用の BookResponse を作っています。

補足

Pydanticでモデルを辞書に変換するメソッドは、バージョン2で model_dump() という名前になりました。バージョン1の dict() は非推奨です。

なお、クライアントは id を送りません(id はリクエスト側Bookではなく、レスポンス側BookResponseで定義されたフィールドであるためです)。サーバーが採番して返す、という形になります。

パスパラメータを用いて書籍情報を取得する

URLの一部を変数として受け取るしくみをパスパラメータと呼びます。パスの中に波かっこ {} で名前を書き、同じ名前の引数に型を付けます(下記のbook_id: intの部分)。

@app.get("/books/{book_id}")
async def get_book(book_id: int) -> BookResponse:
    if book_id not in books:
        raise HTTPException(status_code=404, detail="指定された書籍が見つかりません")
    return books[book_id]

book_id: int と型を付けておくと、FastAPI は URL の中の文字列(たとえば /books/3"3")を整数 3 に変換してから渡してくれます。整数に変換できない値(たとえば /books/abc)が来た場合は、自動的に先ほどの 422 エラーになります。

存在しない id が指定されたときは、raise HTTPException(...) という1行で 404 を返しています。raise は「エラーを発生させる」命令です(詳しくは次の節で説明します)。HTTPException を使うには from fastapi import HTTPException の読み込みが必要です。

エンドポイント関数を定義する順番に注意

固定の文字列のパスと、変数(パスパラメータ)のパスが重なるときは、固定のほうを先に書きます。たとえば GET /books/recent のような固定パスを GET /books/{book_id} より後に書くと、recent という文字列が book_id の値だと解釈されてしまいます。FastAPI は、書いた順番に上から一致を試すためです。

# NG:この順番だと /books/recent も book_id=recent として処理される
@app.get("/books/{book_id}")
async def get_book(book_id: int) -> BookResponse:
    ...

@app.get("/books/recent")
async def get_recent_books() -> list[BookResponse]:
    ...

# OK:固定パスを先に書く
@app.get("/books/recent")
async def get_recent_books() -> list[BookResponse]:
    ...

@app.get("/books/{book_id}")
async def get_book(book_id: int) -> BookResponse:
    ...

HTTPException でエラーを返す

API は、正常な結果だけでなく、「見つからなかった」「リクエストがおかしい」といった状況も、ふさわしい HTTP ステータスコードとともに返す必要があります。FastAPI では、そのために HTTPException を使います。前節の get_book では、存在しない id のときに次の1行でエラーを返していました。

raise HTTPException(status_code=404, detail="指定された書籍が見つかりません")

ここで使った HTTPException は、from fastapi import HTTPException で読み込みます。渡している引数は2つです。

引数説明
status_code返す HTTP ステータスコードです。「見つからない」は 404、「リクエストの内容が不正」は 400 のように、状況に合ったものを選びます。
detailエラーの説明です。文字列のほか、JSON にできる値(辞書やリスト)も渡せます。クライアントには {"detail": "..."} という形の JSON で返ります。

この2つ以外に headers(レスポンスヘッダーの追加)も指定できますが、認証まわりなど発展的な場面で使うものなので、本記事では扱いません。

ここで大切なのは、HTTPExceptionreturn(返す)のではなく raise(発生させる)という点です。HTTPExceptionはPythonの例外の一種で、raiseするとその場で処理が中断され、FastAPI がそれを受け取ってHTTP のエラー応答に変換してくれます。

raise で発生させることには利点もあります。エンドポイント関数から呼び出した別の補助関数の中で raise HTTPException(...) しても、その時点でリクエストの処理を打ち切ってエラーを返せます。「エラーだったかどうか」を表す値をいちいち戻り値で受け渡して、呼び出し元まで運ぶ必要がありません。

動作させてみる

ここまでをまとめた main.py の全体は、次のとおりです。これだけで動作します。

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class Book(BaseModel):
    title: str
    author: str
    year: int | None = None

class BookResponse(Book):
    id: int

books: dict[int, BookResponse] = {}

@app.get("/books")
async def list_books() -> list[BookResponse]:
    return list(books.values())

@app.get("/books/{book_id}")
async def get_book(book_id: int) -> BookResponse:
    if book_id not in books:
        raise HTTPException(status_code=404, detail="指定された書籍が見つかりません")
    return books[book_id]

@app.post("/books")
async def create_book(book: Book) -> BookResponse:
    new_id = max(books.keys(), default=0) + 1
    new_book = BookResponse(id=new_id, **book.model_dump())
    books[new_id] = new_book
    return new_book

次のコマンドで起動します。

fastapi dev main.py
# uv の 場合
uv run fastapi dev main.py

自動生成ドキュメント(/docs)で動かす

FastAPI は、これまで書いてきた型ヒントや Pydantic モデルをもとに、API のドキュメントを自動で生成します。

項目アクセス内容
Swagger UIhttp://127.0.0.1:8000/docs 各エンドポイントがどんな入力を受け取り、どんな出力を返すかの一覧を確認でき、ブラウザ上から実際にリクエストを送って試せます。
ReDochttp://127.0.0.1:8000/redoc こちらは読みやすく整理された、閲覧向けの仕様表示です。

たとえば /docsPOST /books を開き、titleauthor を入力して実行すると、id の付いた BookResponse が返ってきます。続けて GET /books/{book_id} にその id を入れれば、登録した書籍を取得できます。存在しない id を入れたときに、404detail のメッセージが返ることも確認できます。

このドキュメントはコードから自動生成されるため、エンドポイントやモデルを変更すれば、ドキュメントの内容も自動で追従します。仕様書を手作業で更新し続ける必要はありません。

まとめ

本記事では、FastAPIを用いた非同期エンドポイントの作成方法について解説しました。以下にポイントをまとめます。

  • FastAPI のエンドポイントは、@app.get(...)@app.post(...) といったデコレータと関数で定義します。async def で書くと非同期の処理を await で待てます。一方、待つ処理がなければ def で書いても構いません(その場合は FastAPI がスレッドで実行します)。
  • リクエストボディは、Pydantic の BaseModel を継承したクラスで受け取ります。初期値のあるフィールドは省略可能になり、型が合わなければ FastAPI が自動で 422 を返します。
  • 戻り値の型注釈(-> BookResponse など)は、メモ書きにとどまらず、レスポンスの型・検証・ドキュメントに使われます。
  • パスパラメータは、パスに {} で書き、同じ名前の引数に型を付けると、自動で変換・検証されます。固定パスは、変数のパスより先に宣言します。
  • エラーは、HTTPExceptionraise して返します(return ではありません)。status_codedetail を指定します。
  • /docs(Swagger UI)と /redoc(ReDoc)が自動生成され、ブラウザから API を試せます。

-Technology
-,