4
3

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とは(復習)

FastAPIは、Pythonで高速かつ簡単にWeb APIを構築するためのモダンなフレームワークです。非同期処理をサポートし、StarletteとPydanticを基盤にしているため、高パフォーマンスで型安全なAPI開発が可能です。自動生成されるOpenAPIドキュメント(Swagger UI)により、APIのテストやドキュメント化が容易です。主に、効率的でスケーラブルなWebアプリケーションやマイクロサービスの構築に使用されます。

image.png

今回作るもの

シンプルな商品検索APIを作ります。

image.png

今回は/docs(swagger UI)にて確認はせず、URL先のJSON形式の表示結果で確認をします。
swagger UIで確認したい場合は、/docsとしてアクセスし、前回の記事と同じような操作で確認してみてください。

クエリパラメータとは?

FastAPIのクエリパラメータは、URLの?key=value形式でデータを受け取る仕組みです。
関数の引数にデフォルト値を設定するだけで、自動的にクエリとして扱われます。
バリデーションやドキュメント生成も自動で行われるのが特徴です。

事前準備

必要なもの

  • Python 3.7以上
  • テキストエディタ(メモ帳でもOK、VS Codeがおすすめ)

インストール

# ターミナル(コマンドプロンプト)で実行
pip install fastapi uvicorn

ベースとなるコード

main.py を作成して、以下を書いてください:

from fastapi import FastAPI

app = FastAPI(title="商品検索API", description="クエリパラメータを学習するAPI")

# 商品データ(実際のアプリではデータベースを使います)
products = [
    {"id": 1, "name": "ノートPC", "price": 80000, "category": "電子機器", "in_stock": True},
    {"id": 2, "name": "マウス", "price": 2000, "category": "電子機器", "in_stock": True},
    {"id": 3, "name": "キーボード", "price": 5000, "category": "電子機器", "in_stock": False},
    {"id": 4, "name": "コーヒー豆", "price": 1500, "category": "食品", "in_stock": True},
    {"id": 5, "name": "マグカップ", "price": 800, "category": "食器", "in_stock": True},
    {"id": 6, "name": "スマートフォン", "price": 120000, "category": "電子機器", "in_stock": True},
    {"id": 7, "name": "緑茶", "price": 500, "category": "食品", "in_stock": False},
    {"id": 8, "name": "お皿", "price": 1200, "category": "食器", "in_stock": True},
]

@app.get("/")
def home():
    return {"message": "商品検索APIへようこそ!", "total_products": len(products)}

実行して動作確認:

uvicorn main:app --reload

結果は以下のようになります:

{
message: "商品検索APIへようこそ!",
total_products: 8
}

Step 1: 基本的なクエリパラメータ

最もシンプルな形

main.py に以下を追加:

@app.get("/products")
def get_products(limit: int = 10):
    return products[:limit]

🔍 基本的なクエリパラメータの解説

関数の引数 = クエリパラメータ

def get_products(limit: int = 10):
  • 関数の引数が自動的にクエリパラメータになります
  • limit: int は「limitというパラメータは整数型」という意味
  • = 10 はデフォルト値(パラメータが指定されない場合の値)

使い方の例

  • http://127.0.0.1:8000/products → limit=10(デフォルト)
  • http://127.0.0.1:8000/products?limit=3 → limit=3
  • http://127.0.0.1:8000/products?limit=20 → limit=20

スライス記法

return products[:limit]
  • products[:3] → 最初の3個を取得
  • products[:10] → 最初の10個を取得

試してみよう!

  1. http://127.0.0.1:8000/products → 全商品表示
  2. http://127.0.0.1:8000/products?limit=3 → 3個だけ表示
  3. http://127.0.0.1:8000/docs → 自動生成された文書で確認

以下のように表示されます:

# http://127.0.0.1:8000/products 全商品表示

{
products: [
{
id: 1,
name: "ノートPC",
price: 80000,
category: "電子機器",
in_stock: true
},
{
id: 2,
name: "マウス",
price: 2000,
category: "電子機器",
in_stock: true
},
{
id: 3,
name: "キーボード",
price: 5000,
category: "電子機器",
in_stock: false
},
{
id: 4,
name: "コーヒー豆",
price: 1500,
category: "食品",
in_stock: true
},
{
id: 5,
name: "マグカップ",
price: 800,
category: "食器",
in_stock: true
},
{
id: 6,
name: "スマートフォン",
price: 120000,
category: "電子機器",
in_stock: true
},
{
id: 7,
name: "緑茶",
price: 500,
category: "食品",
in_stock: false
},
{
id: 8,
name: "お皿",
price: 1200,
category: "食器",
in_stock: true
}
],
count: 8
}

Step 2: 複数のクエリパラメータ

価格範囲で絞り込み

main.py に以下を追加:

@app.get("/products/search")
def search_products(min_price: int = 0, max_price: int = 999999):
    result = []
    for product in products:
        if min_price <= product["price"] <= max_price:
            result.append(product)
    return {"products": result, "count": len(result)}

🔍 複数パラメータの解説

複数の引数

def search_products(min_price: int = 0, max_price: int = 999999):
  • 複数の引数を書くと、それぞれがクエリパラメータになります
  • min_pricemax_price の2つのパラメータを受け取れます

URLでの指定方法

  • ?min_price=1000&max_price=5000
  • & で複数のパラメータを繋げます

条件での絞り込み

if min_price <= product["price"] <= max_price:
  • Pythonの連鎖比較:「min_price以上、max_price以下」

試してみよう!

  1. http://127.0.0.1:8000/products/search?min_price=1000&max_price=5000
  2. http://127.0.0.1:8000/products/search?max_price=2000
  3. http://127.0.0.1:8000/products/search?min_price=10000
# http://127.0.0.1:8000/products/search?min_price=1000&max_price=5000

{
products: [
{
id: 2,
name: "マウス",
price: 2000,
category: "電子機器",
in_stock: true
},
{
id: 3,
name: "キーボード",
price: 5000,
category: "電子機器",
in_stock: false
},
{
id: 4,
name: "コーヒー豆",
price: 1500,
category: "食品",
in_stock: true
},
{
id: 8,
name: "お皿",
price: 1200,
category: "食器",
in_stock: true
}
],
count: 4
}

Step 3: 文字列のクエリパラメータ

商品名で検索

main.py に以下を追加:

@app.get("/products/search/name")
def search_by_name(q: str = ""):
    if q == "":
        return {"message": "検索キーワードを入力してください", "example": "?q=ノート"}
    
    result = []
    for product in products:
        if q.lower() in product["name"].lower():
            result.append(product)
    
    return {
        "keyword": q,
        "products": result, 
        "count": len(result)
    }

🔍 文字列パラメータの解説

文字列型のパラメータ

def search_by_name(q: str = ""):
  • q: str で文字列型のパラメータを定義
  • = "" で空文字列をデフォルトに設定

大文字小文字を無視した検索

if q.lower() in product["name"].lower():
  • .lower() で小文字に変換
  • 「ノート」でも「のーと」でも「NOTE」でもマッチするように

in 演算子

"ノート" in "ノートPC"  # True
"マウス" in "ノートPC"  # False
  • 文字列に特定の文字が含まれているかチェック

試してみよう!

  1. http://127.0.0.1:8000/products/search/name?q=ノート
  2. http://127.0.0.1:8000/products/search/name?q=コーヒー
  3. http://127.0.0.1:8000/products/search/name?q=PC
# http://127.0.0.1:8000/products/search/name?q=ノート
{
keyword: "ノート",
products: [
{
id: 1,
name: "ノートPC",
price: 80000,
category: "電子機器",
in_stock: true
}
],
count: 1
}

Step 4: 真偽値(bool)のクエリパラメータ

在庫の有無で絞り込み

main.py に以下を追加:

@app.get("/products/stock")
def get_products_by_stock(in_stock: bool = True):
    result = []
    for product in products:
        if product["in_stock"] == in_stock:
            result.append(product)
    
    status = "在庫あり" if in_stock else "在庫なし"
    return {
        "status": status,
        "products": result, 
        "count": len(result)
    }

🔍 真偽値パラメータの解説

bool 型のパラメータ

def get_products_by_stock(in_stock: bool = True):
  • bool 型は True または False の値を取ります

URLでの指定方法

  • ?in_stock=true → True
  • ?in_stock=false → False
  • ?in_stock=1 → True
  • ?in_stock=0 → False

FastAPIが自動的に文字列を真偽値に変換してくれます!

三項演算子

status = "在庫あり" if in_stock else "在庫なし"
  • 条件 if 判定 else 条件 の形
  • in_stock が True なら「在庫あり」、False なら「在庫なし」

試してみよう!

  1. http://127.0.0.1:8000/products/stock?in_stock=true → 在庫ありの商品
  2. http://127.0.0.1:8000/products/stock?in_stock=false → 在庫なしの商品
  3. http://127.0.0.1:8000/products/stock → デフォルト(在庫あり)
# http://127.0.0.1:8000/products/stock?in_stock=true 在庫ありの商品

{
status: "在庫あり",
products: [
{
id: 1,
name: "ノートPC",
price: 80000,
category: "電子機器",
in_stock: true
},
{
id: 2,
name: "マウス",
price: 2000,
category: "電子機器",
in_stock: true
},
{
id: 4,
name: "コーヒー豆",
price: 1500,
category: "食品",
in_stock: true
},
{
id: 5,
name: "マグカップ",
price: 800,
category: "食器",
in_stock: true
},
{
id: 6,
name: "スマートフォン",
price: 120000,
category: "電子機器",
in_stock: true
},
{
id: 8,
name: "お皿",
price: 1200,
category: "食器",
in_stock: true
}
],
count: 6
}

Step 5: Enumを使った選択肢の制限

カテゴリで絞り込み(選択肢を制限)

main.py の先頭に追加:

from enum import Enum

class Category(str, Enum):
    ELECTRONICS = "電子機器"
    FOOD = "食品"
    TABLEWARE = "食器"

そして、以下のエンドポイントを追加:

@app.get("/products/category")
def get_products_by_category(category: Category):
    result = []
    for product in products:
        if product["category"] == category.value:
            result.append(product)
    
    return {
        "category": category.value,
        "products": result, 
        "count": len(result)
    }

🔍 Enumの解説

Enum とは?

class Category(str, Enum):
    ELECTRONICS = "電子機器"
    FOOD = "食品"
    TABLEWARE = "食器"
  • 選択肢を制限するためのクラス
  • 無効な値が入力されると、自動的にエラーになります
  • API文書に選択肢が自動表示されます

パラメータでの使用

def get_products_by_category(category: Category):
  • Category 型なので、定義した値のみ受け付けます

.value の使用

if product["category"] == category.value:
  • category.value で実際の文字列値を取得

試してみよう!

  1. http://127.0.0.1:8000/products/category?category=電子機器
  2. http://127.0.0.1:8000/products/category?category=食品
  3. http://127.0.0.1:8000/products/category?category=無効な値 → エラーになる!
# http://127.0.0.1:8000/products/category?category=電子機器

{
category: "電子機器",
products: [
{
id: 1,
name: "ノートPC",
price: 80000,
category: "電子機器",
in_stock: true
},
{
id: 2,
name: "マウス",
price: 2000,
category: "電子機器",
in_stock: true
},
{
id: 3,
name: "キーボード",
price: 5000,
category: "電子機器",
in_stock: false
},
{
id: 6,
name: "スマートフォン",
price: 120000,
category: "電子機器",
in_stock: true
}
],
count: 4
}

Step 6: オプション(Optional)パラメータ

より柔軟な検索機能

main.py の先頭に Optional を追加:

from typing import Optional

そして、以下のエンドポイントを追加:

@app.get("/products/advanced-search")
def advanced_search(
    name: Optional[str] = None,
    category: Optional[Category] = None,
    min_price: Optional[int] = None,
    max_price: Optional[int] = None,
    in_stock: Optional[bool] = None
):
    result = products.copy()  # 全商品から開始
    
    # 商品名でフィルタ
    if name is not None:
        result = [p for p in result if name.lower() in p["name"].lower()]
    
    # カテゴリでフィルタ  
    if category is not None:
        result = [p for p in result if p["category"] == category.value]
    
    # 最低価格でフィルタ
    if min_price is not None:
        result = [p for p in result if p["price"] >= min_price]
    
    # 最高価格でフィルタ
    if max_price is not None:
        result = [p for p in result if p["price"] <= max_price]
    
    # 在庫状況でフィルタ
    if in_stock is not None:
        result = [p for p in result if p["in_stock"] == in_stock]
    
    return {
        "filters": {
            "name": name,
            "category": category.value if category else None,
            "min_price": min_price,
            "max_price": max_price,
            "in_stock": in_stock
        },
        "products": result,
        "count": len(result)
    }

🔍 Optionalパラメータの解説

Optional[型] とは?

name: Optional[str] = None
  • 指定してもしなくてもよいパラメータ
  • 指定しない場合は None になります
  • Optional[str] = 「文字列またはNone」

is not None でのチェック

if name is not None:
    # nameが指定された場合のみ実行
  • None でない場合のみフィルタを適用
  • つまり、パラメータが指定された場合のみ処理

リスト内包表記

result = [p for p in result if name.lower() in p["name"].lower()]
  • for p in result でループ
  • if 条件 で条件に合うもののみ抽出
  • とても便利なPythonの記法です!

試してみよう!

  1. http://127.0.0.1:8000/products/advanced-search?name=ノート&in_stock=true
  2. http://127.0.0.1:8000/products/advanced-search?category=電子機器&max_price=10000
  3. http://127.0.0.1:8000/products/advanced-search?min_price=1000&max_price=5000&in_stock=true
# http://127.0.0.1:8000/products/advanced-search?name=ノート&in_stock=true
{
filters: {
name: "ノート",
category: null,
min_price: null,
max_price: null,
in_stock: true
},
products: [
{
id: 1,
name: "ノートPC",
price: 80000,
category: "電子機器",
in_stock: true
}
],
count: 1
}

Step 7: Queryを使った詳細設定

より詳細なパラメータ設定

main.py の先頭に Query を追加:

from fastapi import FastAPI, Query

以下のエンドポイントを追加:

@app.get("/products/detailed-search")
def detailed_search(
    q: str = Query(
        default="",
        min_length=0,
        max_length=50,
        description="商品名の検索キーワード",
        example="ノートPC"
    ),
    limit: int = Query(
        default=10,
        ge=1,  # 1以上
        le=100,  # 100以下
        description="取得する商品数の上限",
        example=5
    ),
    offset: int = Query(
        default=0,
        ge=0,
        description="スキップする商品数(ページング用)",
        example=0
    )
):
    # 検索処理
    if q:
        filtered_products = [
            p for p in products 
            if q.lower() in p["name"].lower()
        ]
    else:
        filtered_products = products
    
    # ページング処理
    total = len(filtered_products)
    result = filtered_products[offset:offset+limit]
    
    return {
        "query": q,
        "total": total,
        "offset": offset,
        "limit": limit,
        "products": result,
        "has_next": offset + limit < total
    }

🔍 Queryクラスの解説

Query() とは?

q: str = Query(
    default="",
    min_length=0,
    max_length=50,
    description="商品名の検索キーワード",
    example="ノートPC"
)
  • クエリパラメータに詳細な制約と説明を付けるためのクラス
  • API文書がより詳しくなります

主要なオプション

  • default: デフォルト値
  • min_length / max_length: 文字列の長さ制限
  • ge / le: 数値の範囲制限(ge=以上、le=以下)
  • description: パラメータの説明文
  • example: API文書での例示

ページング処理

result = filtered_products[offset:offset+limit]
  • offset: 何件スキップするか
  • limit: 何件取得するか
  • 例:offset=10, limit=5 → 11-15番目の商品を取得

試してみよう!

  1. http://127.0.0.1:8000/products/detailed-search?q=電子&limit=3
  2. http://127.0.0.1:8000/products/detailed-search?offset=3&limit=2
  3. /docs でパラメータの説明を確認!
# http://127.0.0.1:8000/products/detailed-search?q=電子&limit=3

{
query: "電子",
total: 0,
offset: 0,
limit: 3,
products: [ ],
has_next: false
}

Step 8: 完成したコード全体

最終的な main.py

from fastapi import FastAPI, Query
from enum import Enum
from typing import Optional



app = FastAPI(title="商品検索API", description="クエリパラメータを学習するAPI")

# カテゴリの定義
class Category(str, Enum):
    ELECTRONICS = "電子機器"
    FOOD = "食品"
    TABLEWARE = "食器"

# 商品データ
products = [
    {"id": 1, "name": "ノートPC", "price": 80000, "category": "電子機器", "in_stock": True},
    {"id": 2, "name": "マウス", "price": 2000, "category": "電子機器", "in_stock": True},
    {"id": 3, "name": "キーボード", "price": 5000, "category": "電子機器", "in_stock": False},
    {"id": 4, "name": "コーヒー豆", "price": 1500, "category": "食品", "in_stock": True},
    {"id": 5, "name": "マグカップ", "price": 800, "category": "食器", "in_stock": True},
    {"id": 6, "name": "スマートフォン", "price": 120000, "category": "電子機器", "in_stock": True},
    {"id": 7, "name": "緑茶", "price": 500, "category": "食品", "in_stock": False},
    {"id": 8, "name": "お皿", "price": 1200, "category": "食器", "in_stock": True},
]

@app.get("/")
def home():
    return {
        "message": "商品検索APIへようこそ!",
        "total_products": len(products),
        "endpoints": {
            "基本": "/products?limit=5",
            "価格検索": "/products/search?min_price=1000&max_price=5000",
            "名前検索": "/products/search/name?q=ノート",
            "在庫検索": "/products/stock?in_stock=true",
            "カテゴリ": "/products/category?category=電子機器",
            "高度検索": "/products/advanced-search",
            "詳細検索": "/products/detailed-search"
        }
    }

@app.get("/products")
def get_products(limit: int = 10):
    """基本的な商品一覧(件数制限付き)"""
    return {
        "products": products[:limit],
        "count": min(limit, len(products))
    }

@app.get("/products/search")
def search_products(min_price: int = 0, max_price: int = 999999):
    """価格範囲で商品を検索"""
    result = []
    for product in products:
        if min_price <= product["price"] <= max_price:
            result.append(product)
    return {"products": result, "count": len(result)}

@app.get("/products/search/name")
def search_by_name(q: str = ""):
    """商品名で検索"""
    if q == "":
        return {"message": "検索キーワードを入力してください", "example": "?q=ノート"}
    
    result = []
    for product in products:
        if q.lower() in product["name"].lower():
            result.append(product)
    
    return {"keyword": q, "products": result, "count": len(result)}

@app.get("/products/stock")
def get_products_by_stock(in_stock: bool = True):
    """在庫状況で商品を絞り込み"""
    result = []
    for product in products:
        if product["in_stock"] == in_stock:
            result.append(product)
    
    status = "在庫あり" if in_stock else "在庫なし"
    return {"status": status, "products": result, "count": len(result)}

@app.get("/products/category")
def get_products_by_category(category: Category):
    """カテゴリで商品を絞り込み(選択肢制限あり)"""
    result = []
    for product in products:
        if product["category"] == category.value:
            result.append(product)
    
    return {"category": category.value, "products": result, "count": len(result)}

@app.get("/products/advanced-search")
def advanced_search(
    name: Optional[str] = None,
    category: Optional[Category] = None,
    min_price: Optional[int] = None,
    max_price: Optional[int] = None,
    in_stock: Optional[bool] = None
):
    """複数条件での高度な検索(すべてオプション)"""
    result = products.copy()
    
    if name is not None:
        result = [p for p in result if name.lower() in p["name"].lower()]
    
    if category is not None:
        result = [p for p in result if p["category"] == category.value]
    
    if min_price is not None:
        result = [p for p in result if p["price"] >= min_price]
    
    if max_price is not None:
        result = [p for p in result if p["price"] <= max_price]
    
    if in_stock is not None:
        result = [p for p in result if p["in_stock"] == in_stock]
    
    return {
        "filters": {
            "name": name,
            "category": category.value if category else None,
            "min_price": min_price,
            "max_price": max_price,
            "in_stock": in_stock
        },
        "products": result,
        "count": len(result)
    }

@app.get("/products/detailed-search")
def detailed_search(
    q: str = Query(
        default="",
        min_length=0,
        max_length=50,
        description="商品名の検索キーワード",
        example="ノートPC"
    ),
    limit: int = Query(
        default=10,
        ge=1,
        le=100,
        description="取得する商品数の上限",
        example=5
    ),
    offset: int = Query(
        default=0,
        ge=0,
        description="スキップする商品数(ページング用)",
        example=0
    )
):
    """詳細設定付きの検索(ページング対応)"""
    # 検索処理
    if q:
        filtered_products = [
            p for p in products 
            if q.lower() in p["name"].lower()
        ]
    else:
        filtered_products = products
    
    # ページング処理
    total = len(filtered_products)
    result = filtered_products[offset:offset+limit]
    
    return {
        "query": q,
        "total": total,
        "offset": offset,
        "limit": limit,
        "products": result,
        "has_next": offset + limit < total
    }

最後に

今回はFastAPI初学者でもわかるようにクエリパラメータについてハンズオン形式での開発を行いました。
もっと詳しい内容は公式ドキュメントなどを読んで開発をしてみてください。

4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?