0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FastAPI入門 — PHPエンジニアがPythonでAPIを書いてみた

0
Posted at

はじめに

データ処理系の学習が一段落したので、次はWeb API。

Python案件でAPIを書く機会があるかもしれないということで、FastAPIを触ってみた。PythonのWebフレームワークはDjango・Flask・FastAPIが主要どころで、「API開発ならFastAPIが今のスタンダード」という意見が多かったのでこれを選んだ。

LaravelでAPIを作り慣れていたので、バリデーション・ルーティング・DI周りをLaravelと比べながら理解した。思想がかなり違う部分もあり、そこが面白かった。


インストール

pip install fastapi uvicorn

uvicornはASGIサーバー。LaravelでいうApache/Nginxのような立ち位置だが、開発サーバーとしても使える。

# 開発サーバーを起動
uvicorn main:app --reload
# main → ファイル名(main.py)
# app  → FastAPIインスタンスの変数名
# --reload → ファイル変更時に自動リロード

最初のAPI

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, World!"}

@app.get("/users/{user_id}")
def read_user(user_id: int):
    return {"user_id": user_id, "name": "田中"}

これだけで動く。Laravelと比べると圧倒的に少ない。

<?php
// Laravel
Route::get('/', function () {
    return response()->json(["message" => "Hello, World!"]);
});

Route::get('/users/{user_id}', [UserController::class, "show"]);

Laravelはルート定義とコントローラーが分離しているが、FastAPIはデコレータでルートと処理を直接紐づける。Flaskに近い感覚。

起動後にhttp://localhost:8000/docsにアクセスするとSwagger UIが自動生成される。これは最初に触れたとき素直に感動した。コードを書くだけでAPIドキュメントが出来上がる。


ルーティング

from fastapi import FastAPI

app = FastAPI()

# GETリクエスト
@app.get("/items")
def list_items():
    return [{"id": 1}, {"id": 2}]

# POSTリクエスト
@app.post("/items")
def create_item():
    return {"message": "作成しました"}

# PUTリクエスト
@app.put("/items/{item_id}")
def update_item(item_id: int):
    return {"item_id": item_id}

# DELETEリクエスト
@app.delete("/items/{item_id}")
def delete_item(item_id: int):
    return {"message": "削除しました"}

パスパラメータ

@app.get("/users/{user_id}/posts/{post_id}")
def get_user_post(user_id: int, post_id: int):
    return {"user_id": user_id, "post_id": post_id}

パスパラメータは関数の引数に型ヒントをつけるだけで、自動でバリデーションとキャストが行われる

GET /users/abc/posts/1
→ 422 Unprocessable Entity(user_idがintでないためエラー)

LaravelだとRoute::get('/users/{user_id}', ...)$request->route('user_id')で受け取り、型チェックは自前でやる必要があった。FastAPIは型ヒントを書くだけで済む。

クエリパラメータ

@app.get("/items")
def list_items(
    page:     int  = 1,
    per_page: int  = 20,
    keyword:  str | None = None,
):
    return {
        "page":     page,
        "per_page": per_page,
        "keyword":  keyword,
    }

パスパラメータ以外の引数は自動でクエリパラメータとして扱われる。

GET /items?page=2&per_page=10&keyword=python
→ {"page": 2, "per_page": 10, "keyword": "python"}

デフォルト値をつければ省略可能、Noneで省略可能かつNull許容。Laravelの$request->query('page', 1)に相当するものが引数定義だけで書ける。


Pydantic — リクエストボディのバリデーション

ここがFastAPIの一番の特徴といってもいい。

Laravelでリクエストのバリデーションをする場合。

<?php
// Laravel FormRequest
class StoreUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            "name"  => ["required", "string", "max:50"],
            "age"   => ["required", "integer", "min:0", "max:150"],
            "email" => ["required", "email"],
        ];
    }
}

FastAPIではPydanticのモデルを使う。

from fastapi  import FastAPI
from pydantic import BaseModel, Field, EmailStr

app = FastAPI()

class UserCreate(BaseModel):
    name:  str         = Field(..., min_length=1, max_length=50)
    age:   int         = Field(..., ge=0, le=150)
    email: EmailStr

@app.post("/users")
def create_user(user: UserCreate):
    # バリデーション済みのデータが user に入っている
    return {"name": user.name, "email": user.email}
# バリデーションエラーのレスポンス(自動生成)
POST /users
{"name": "", "age": -1, "email": "invalid"}

→ 422 Unprocessable Entity
{
  "detail": [
    {"loc": ["body", "name"], "msg": "String should have at least 1 character"},
    {"loc": ["body", "age"],  "msg": "Input should be greater than or equal to 0"},
    {"loc": ["body", "email"],"msg": "value is not a valid email address"}
  ]
}

型ヒントを書くだけでバリデーションとエラーレスポンスが自動で用意される。EmailStrを使うにはpydantic[email]が必要。

pip install pydantic[email]

レスポンスモデルの定義

from pydantic import BaseModel

class UserResponse(BaseModel):
    id:    int
    name:  str
    email: str
    # パスワードなど返したくないフィールドは含めない

class UserCreate(BaseModel):
    name:     str
    email:    str
    password: str

@app.post("/users", response_model=UserResponse)
def create_user(user: UserCreate):
    # DBに保存する処理(省略)
    saved_user = {"id": 1, "name": user.name, "email": user.email, "password": user.password}
    return saved_user
    # response_modelによってpasswordは自動的に除外される

response_modelを指定するとレスポンスのフィルタリングとドキュメント生成が自動で行われる。Laravelのリソースクラスに近い役割。


依存性注入(DI)

FastAPIにはシンプルなDIの仕組みが組み込まれている。

共通処理をDependsで注入する

from fastapi import FastAPI, Depends, HTTPException, Header

app = FastAPI()

# 認証処理を共通化
def verify_token(authorization: str = Header(...)):
    if authorization != "Bearer secret-token":
        raise HTTPException(status_code=401, detail="認証エラー")
    return authorization

@app.get("/protected")
def protected_route(token: str = Depends(verify_token)):
    return {"message": "認証済みです"}

Depends()に関数を渡すと、その関数の返り値が引数に注入される。認証・DBセッション・ページネーションなど共通処理を切り出してどのエンドポイントにも適用できる。

DBセッションのDI(よくあるパターン)

from fastapi import Depends

# DBセッションを提供するdependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/users/{user_id}")
def get_user(user_id: int, db = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if user is None:
        raise HTTPException(status_code=404, detail="ユーザーが見つかりません")
    return user

yieldを使うとリクエスト処理後にクリーンアップが走る。LaravelのServiceContainerとは思想が違うが「共通処理を関数として渡す」というシンプルな設計で直感的だった。


エラーハンドリング

from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/users/{user_id}")
def get_user(user_id: int):
    if user_id <= 0:
        raise HTTPException(
            status_code=400,
            detail="user_idは1以上を指定してください",
        )
    if user_id > 100:
        raise HTTPException(
            status_code=404,
            detail="ユーザーが見つかりません",
        )
    return {"user_id": user_id}

HTTPExceptionraiseするとそのままJSONエラーレスポンスになる。

カスタム例外ハンドラー

from fastapi          import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

class DomainError(Exception):
    def __init__(self, message: str, code: int = 400):
        self.message = message
        self.code    = code

@app.exception_handler(DomainError)
async def domain_error_handler(request: Request, exc: DomainError):
    return JSONResponse(
        status_code=exc.code,
        content={"error": exc.message},
    )

@app.get("/items/{item_id}")
def get_item(item_id: int):
    if item_id < 0:
        raise DomainError("IDは0以上を指定してください")
    return {"item_id": item_id}

LaravelのHandler.phpでカスタム例外を処理するのと同じことがexception_handlerデコレータで書ける。


ルーターで分割する

エンドポイントが増えたらファイルを分割する。

myapi/
├── main.py
└── routers/
    ├── __init__.py
    ├── users.py
    └── items.py
# routers/users.py
from fastapi import APIRouter

router = APIRouter(prefix="/users", tags=["users"])

@router.get("/")
def list_users():
    return [{"id": 1, "name": "田中"}]

@router.get("/{user_id}")
def get_user(user_id: int):
    return {"user_id": user_id}

@router.post("/")
def create_user():
    return {"message": "作成しました"}
# main.py
from fastapi         import FastAPI
from routers         import users, items

app = FastAPI()

app.include_router(users.router)
app.include_router(items.router)

APIRouterはLaravelのRoute::group()に相当する。prefixでURLのプレフィックスを、tagsでSwagger UIのグループ名を設定できる。


LaravelとFastAPIの比較

項目 Laravel FastAPI
ルーティング routes/api.php デコレータ(@app.get
コントローラー クラス必須 関数でOK
バリデーション FormRequest / Validator Pydanticモデル
レスポンス整形 APIResource response_model
DI ServiceContainer Depends()
認証 Sanctum / Passport Depends()で自作 or ライブラリ
ORMデフォルト Eloquent なし(SQLAlchemyが一般的)
APIドキュメント 別途Swaggerを設定 自動生成(/docs)
非同期 基本同期(Octane使用) ネイティブ対応(async/await)

Laravelはフルスタックフレームワークでほぼ全部入りだが、FastAPIはAPIに特化していてシンプル。ORMや認証は別途選んで組み合わせる設計。


まとめ

  • デコレータでルートと処理を直接紐づける
  • 型ヒントを書くだけでバリデーション・キャスト・ドキュメントが自動で生成される
  • Pydanticモデルでリクエスト/レスポンスの型が明示できる
  • Depends()でDIを実現する
  • /docsでSwagger UIが自動で使える

「型ヒントを書くだけで多くのことが自動化される」という設計思想が一貫していて、書いていて気持ちよかった。Laravelほど「何でもある」ではないが、その分シンプルで学習コストが低い。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?