【Python】FastAPIで料金プラン見積もりシミュレーターを作ってみた

【Python】FastAPIで料金プラン見積もりシミュレーターを作ってみた

公開: 更新:
CodeCampが提供するDX人材育成が可能なプログラミングやITが学べる公開講座

「スタンダードプランで15人、年払いにしたら結局いくら?」——料金表と電卓を行き来しながら見積もりを出すのは、意外と面倒な作業です。そこでPythonのFastAPIを使って、プラン・人数・オプション・支払い方法を選ぶと税込金額まで一発で表示される料金プラン見積もりシミュレーターを作ってみました。

テンプレートエンジンもデータベースも使わない1ファイル構成なので、FastAPI入門の題材にもちょうどいいはずです。実際に4種類の画面で表示と絞り込みの動作を確認するところまで、コードを抜粋しながら解説します。

【Python】FastAPIで料金プラン見積もりシミュレーターを作ってみた|概要動画
動画の内容をテキストで確認する

オープニング。FastAPIを使った実践内容を約34秒で紹介します。概要紹介。

この動画では、PythonのFastAPIで、料金プランの見積もりシミュレーターを作ります。具体的にやること。具体的には、FastAPIでフォーム画面と計算処理を実装します。

小計から年払い割引を引き、最後に消費税を加えて税込み額を出します。実装環境・必須アプリ。実装環境と必須アプリは、Windows 11 Pro / Python 3.13.3 / FastAPIです。

実際のキャプチャ動画。実際の画面で操作を確認します。検証の結果、4種類の画面で表示と絞り込みの動作を確認できました。

エンディング。研修ならCodeCampで検索。

Python FastAPIで料金見積もりシミュレーターを作る方針

今回作るのは、プラン(ライト/スタンダード/プレミアム)・利用人数・オプション・月払いか年払いかを選ぶと、税込の見積もり金額を表示するWebアプリです。画面は入力フォームの「/」と結果表示の「/estimate」の2つだけに絞りました。

方針はシンプルで、FastAPIのGETエンドポイントを2つ用意し、料金マスタはPythonの辞書で定義します。データベースを使わないので準備するものが少なく、HTMLもテンプレートエンジンを使わずPythonの文字列で組み立てるため、外部依存のない1ファイル構成に収まりました。

見積もり計算はcalculate関数に一本化し、小計→年払い割引→消費税の順に、円未満を切り捨てる整数計算で行う仕様です。お金の計算は適用順と丸め方で結果が変わるため、コードを書く前にこの順序を決めておきました。

このセクションの用語

FastAPI
PythonでWebアプリやAPIを作るためのフレームワーク。型ヒントを活かして、URLで受け取った値を自動で変換・検証してくれるのが特徴です。
エンドポイント
「/」や「/estimate」のような、アプリが応答するURLの受け口のこと。1つのURLに1つの処理関数を対応させます。
クエリパラメータ
URLの「?」以降に付けるplan=lightのようなキーと値の組。ページの再読み込みだけで条件をサーバーに渡せます。
入力から結果表示までの処理の流れ。すべてGETリクエストで完結する フォーム画面「/」で条件を選ぶGETで「/estimate」に送信calculate関数で金額を計算Python文字列でHTMLを組み立て見積もり結果ページを表示
入力から結果表示までの処理の流れ。すべてGETリクエストで完結する
さらに詳しく学べるPython研修の詳細はこちら

PythonでFastAPIのコードを生成したプロンプトと作り方

この記事のコードはClaude Codeに書かせたもので、別のAIであるCodexにレビューさせています。以下は、その際に使った指示の内容です。

料金計算は仕様の曖昧さがそのまま金額のズレになるため、プラン・オプション・割引率をすべて具体的な数字で指定しました。とくに「小計から割引を引き、そのあと消費税を加える」と順番まで固定したのがポイントで、これがないと税込み額に割引をかける実装が混ざります。

依存をfastapiとuvicornだけに絞ったのは、生成コードを1ファイルに収めて写経から起動までの距離を最短にするためです。テンプレートエンジンやDBを禁止すると、読者はFastAPI本体の書き方だけに集中できます。

生成プロンプトで工夫したポイント

  • 計算の順序は「小計 → 年払い割引を引く → 消費税10%を加える」に固定し:順番を明記しないと税込み額に割引をかける実装が混ざり、合計が数十円ずれることがあるためです。
  • 割引額と税額は円未満を切り捨てた整数で扱う:浮動小数点のまま計算すると端数の出方が揺れるので、金額を整数に確定させて誰が実行しても同じ結果になるようにします。
  • 3人まで込み・追加1人500円:実際のSaaSに多い「基本料+超過課金」の段階制を数字で示すと、超過分だけを課金する計算が正しく生成されやすくなります。
  • 使うのはfastapiとuvicornだけ:依存が2つに絞られるとコードが1ファイルで完結し、環境構築でつまずく余地も小さくなるからです。

同じコードを作れる生成プロンプト(コピペ用・全文)

以下のプロンプトは、そのままコピペして同じものが作れるように整えた全文です。

PythonのFastAPIで、SaaSの料金プランを想定した見積もりシミュレーターのWebアプリを1ファイルで作ってください。 【料金の仕様】 - プランは3種類:ライト(月額5,000円・3人まで込み・追加1人500円)、スタンダード(月額15,000円・10人まで込み・追加1人800円)、プレミアム(月額50,000円・30人まで込み・追加1人1,200円) - オプションは3種類:優先サポート(+3,000円)、自動バックアップ(+2,000円)、SSO連携(+5,000円)。複数選択できる - 支払いは月払いか年払いを選べて、年払いは小計から15%割引する - 計算の順序は「小計 → 年払い割引を引く → 消費税10%を加える」に固定し、割引額と税額は円未満を切り捨てた整数で扱う 【画面の仕様】 - トップページ(/)に、プランのラジオボタン・利用人数の数値入力・オプションのチェックボックス・支払いサイクルの選択を持つフォームを置く - フォームを送信すると/estimateに遷移し、基本料金・追加ユーザー料金・オプション料金・小計・割引・消費税・税込み合計を内訳表で表示する - 存在しないプラン名が来たらエラーページを返し、人数は1〜500人の範囲に丸める 【実装の条件】 - 使うのはfastapiとuvicornだけ。テンプレートエンジンやデータベースは使わず、HTMLはPythonの文字列で組み立てる - 見積もり計算は1つの関数にまとめ、uvicornで起動してブラウザから動作確認できる形にする
さらに詳しく学べるPython研修の詳細はこちら

Python・FastAPIの環境構築(Windows 11/PowerShellで検証)

この記事のセットアップ手順と掲載コードは、Windows 11 Pro、PowerShell 5.1、Python 3.13.3で動作確認しています。仮想環境を有効化せず、その中のPythonを直接指定するため、以下のコマンドはPowerShellとコマンドプロンプト(cmd)の両方で使えます。

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install fastapi uvicorn

macOS・Linuxでは仮想環境内のPythonパスが異なります。今回の動作確認環境とは異なるため、以下は環境差分を補う参考手順です。

python3 -m venv .venv
./.venv/bin/python -m pip install fastapi uvicorn
  • 起動は.\.venv\Scripts\python.exe -m uvicorn estimate_app:app --reloadで行い、ブラウザでhttp://127.0.0.1:8000を開くと確認できます。
  • サーバーはCtrl+Cで停止します。ポート8000が使用中なら--port 8001のように別ポートを指定してください。

Python研修一覧はこちら

目的に合うPython研修を一覧形式から探したい方は、ぜひご利用ください。

Python研修を比較する

Java研修一覧はこちら

目的に合うJava研修を一覧形式から探したい方は、ぜひご利用ください。

Java研修を比較する

PHP研修一覧はこちら

目的に合うPHP研修を一覧形式から探したい方は、ぜひご利用ください。

PHP研修を比較する

新入社員研修

目的に合う新入社員研修を一覧形式から探したい方は、ぜひご利用ください。

新入社員研修を比較する

全ての研修からも探したい方はこちら

Pythonのcalculate関数で見積もり計算を実装する

コード全体は「料金マスタの定義」「calculate関数」「HTML組み立てのrender_page」「2つのエンドポイント」という4ブロック構成です。上から順に読むと、データ→計算→表示という流れがそのまま追えるように並べました。

工夫が要ったのはオプションの複数選択です。options=support&options=backupのように同じ名前のクエリパラメータを複数並べて送り、受け取る側では引数にQuery(default=[])を指定してリストとしてまとめて受け取ります。

この書き方は自己流ではなく、FastAPIの公式ドキュメントで案内されている方法です。Queryを明示しないとリストはリクエストボディ扱いになってしまうため、GETで複数値を受けるなら必須の指定になります。

それでは、押さえておけば全体が分かる要点だけをコードから抜粋して見ていきます。

このセクションの用語

辞書(dict)
キーと値をペアで持つPythonのデータ型。PLANS["light"]のようにキーを指定して値を取り出せます。
Query
FastAPIでクエリパラメータの受け取り方を細かく指定する仕組み。Query(default=[])とすると、同名パラメータの複数指定をリストで受け取れます。
リスト内包表記
[x for x in ... if 条件]の形で、既存のリストから条件に合う要素だけを集めた新しいリストを1行で作る書き方。
f文字列
先頭にfを付けた文字列の中に{変数}を書くと、その場で値が埋め込まれるPythonの記法。HTMLの組み立てにも使えます。
from fastapi import FastAPI, Query
from fastapi.responses import HTMLResponse

# 料金マスタの定義
PLANS = {
    "light": {"name": "ライト", "base": 5000, "included": 3, "per_user": 500},
    "standard": {"name": "スタンダード", "base": 15000, "included": 10, "per_user": 800},
    "premium": {"name": "プレミアム", "base": 50000, "included": 30, "per_user": 1200},
}
OPTIONS = {
    "support": {"name": "優先サポート", "price": 3000},
    "backup": {"name": "自動バックアップ", "price": 2000},
    "sso": {"name": "SSO連携", "price": 5000},
}
YEARLY_DISCOUNT_RATE = 0.15  # 年払いは小計から15%割引
TAX_RATE = 0.10  # 消費税10%

# アプリ本体と動作確認したい画面の一覧
app = FastAPI(title="料金プラン見積もりシミュレーター")
ROUTES = [
    "/",
    "/estimate?plan=standard&users=15&options=support&options=backup&billing=yearly",
    "/estimate?plan=light&users=3&billing=monthly",
    "/estimate?plan=premium&users=40&options=sso&billing=yearly",
]

# 見積もり計算(小計 → 年払い割引 → 消費税の順で適用する)
def calculate(plan_key: str, users: int, option_keys: list[str], billing: str) -> dict:
    plan = PLANS[plan_key]
    extra_users = max(0, users - plan["included"])
    user_fee = extra_users * plan["per_user"]
    selected_options = [OPTIONS[k] for k in option_keys if k in OPTIONS]
    option_fee = sum(o["price"] for o in selected_options)
    subtotal = plan["base"] + user_fee + option_fee
    discount = int(subtotal * YEARLY_DISCOUNT_RATE) if billing == "yearly" else 0
    tax = int((subtotal - discount) * TAX_RATE)
    return {
        "plan": plan,
        "users": users,
        "extra_users": extra_users,
        "user_fee": user_fee,
        "options": selected_options,
        "option_fee": option_fee,
        "subtotal": subtotal,
        "discount": discount,
        "tax": tax,
        "total": subtotal - discount + tax,
        "billing": billing,
    }

# ページ共通のHTMLレイアウト
def render_page(title: str, body: str) -> str:
    return f"""<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
  body {{ font-family: sans-serif; background: #f4f6fb; margin: 0; color: #223; }}
  main {{ max-width: 640px; margin: 40px auto; background: #fff; border-radius: 12px;
          padding: 32px; box-shadow: 0 2px 10px rgba(0,0,0,.08); }}
  h1 {{ font-size: 1.4rem; border-left: 6px solid #2563eb; padding-left: 12px; }}
  fieldset {{ border: 1px solid #d5dbe7; border-radius: 8px; margin-bottom: 20px; }}
  legend {{ font-weight: bold; padding: 0 8px; }}
  label {{ display: block; margin: 8px 0; }}
  input[type=number] {{ width: 6em; padding: 4px; }}
  button {{ background: #2563eb; color: #fff; border: none; border-radius: 8px;
            padding: 12px 32px; font-size: 1rem; cursor: pointer; }}
  table {{ width: 100%; border-collapse: collapse; margin: 16px 0; }}
  th, td {{ border-bottom: 1px solid #e2e8f0; padding: 10px 8px; text-align: left; }}
  td.amount {{ text-align: right; font-variant-numeric: tabular-nums; }}
  tr.total td {{ font-size: 1.2rem; font-weight: bold; color: #2563eb; }}
  a {{ color: #2563eb; }}
</style>
</head>
<body><main>{body}</main></body>
</html>"""

# 入力フォーム画面
@app.get("/", response_class=HTMLResponse)
def index() -> str:
    plan_inputs = "".join(
        f'<label><input type="radio" name="plan" value="{key}"'
        f'{" checked" if key == "standard" else ""}>'
        f'{p["name"]}:月額{p["base"]:,}円({p["included"]}人まで込み、追加1人{p["per_user"]:,}円)</label>'
        for key, p in PLANS.items()
    )
    option_inputs = "".join(
        f'<label><input type="checkbox" name="options" value="{key}">'
        f'{o["name"]}(+{o["price"]:,}円/月)</label>'
        for key, o in OPTIONS.items()
    )
    body = f"""
<h1>料金プラン見積もりシミュレーター</h1>
<p>プランと利用条件を選んで「見積もりを計算」を押してください。</p>
<form action="/estimate" method="get">
  <fieldset><legend>プラン</legend>{plan_inputs}</fieldset>
  <fieldset><legend>利用人数</legend>
    <label>ユーザー数 <input type="number" name="users" value="10" min="1" max="500"> 人</label>
  </fieldset>
  <fieldset><legend>オプション</legend>{option_inputs}</fieldset>
  <fieldset><legend>支払いサイクル</legend>
    <label><input type="radio" name="billing" value="monthly" checked>月払い</label>
    <label><input type="radio" name="billing" value="yearly">年払い(小計から15%割引)</label>
  </fieldset>
  <button type="submit">見積もりを計算</button>
</form>"""
    return render_page("料金プラン見積もりシミュレーター", body)

# 見積もり結果画面
@app.get("/estimate", response_class=HTMLResponse)
def estimate(
    plan: str = "standard",
    users: int = 1,
    options: list[str] = Query(default=[]),
    billing: str = "monthly",
) -> str:
    if plan not in PLANS:
        return render_page(
            "エラー",
            '<h1>エラー</h1><p>不明なプランが指定されました。</p><p><a href="/">フォームに戻る</a></p>',
        )
    users = min(max(users, 1), 500)
    result = calculate(plan, users, options, billing)

    option_rows = "".join(
        f'<tr><td>オプション:{o["name"]}</td><td class="amount">{o["price"]:,}円</td></tr>'
        for o in result["options"]
    ) or '<tr><td>オプション</td><td class="amount">0円</td></tr>'
    discount_row = (
        f'<tr><td>年払い割引(15%)</td><td class="amount">-{result["discount"]:,}円</td></tr>'
        if result["billing"] == "yearly"
        else ""
    )
    billing_label = "年払い" if result["billing"] == "yearly" else "月払い"
    body = f"""
<h1>見積もり結果</h1>
<p>{result["plan"]["name"]}プラン / {result["users"]}人 / {billing_label}</p>
<table>
  <tr><td>基本料金({result["plan"]["included"]}人まで込み)</td><td class="amount">{result["plan"]["base"]:,}円</td></tr>
  <tr><td>追加ユーザー料金({result["extra_users"]}人 × {result["plan"]["per_user"]:,}円)</td><td class="amount">{result["user_fee"]:,}円</td></tr>
  {option_rows}
  <tr><td>小計</td><td class="amount">{result["subtotal"]:,}円</td></tr>
  {discount_row}
  <tr><td>消費税(10%)</td><td class="amount">{result["tax"]:,}円</td></tr>
  <tr class="total"><td>月額合計(税込)</td><td class="amount">{result["total"]:,}円</td></tr>
</table>
<p><a href="/">条件を変えて再計算する</a></p>"""
    return render_page("見積もり結果", body)

コード全文は上の折り畳みに入れてあるので、全部を上から読む必要はありません。ここでは特に重要な部分だけを抜き出して、何をしているのか順番に見ていきます。

PLANS辞書で料金マスタを一元管理する

PLANS = {
    "light": {"name": "ライト", "base": 5000, "included": 3, "per_user": 500},
    "standard": {"name": "スタンダード", "base": 15000, "included": 10, "per_user": 800},
    "premium": {"name": "プレミアム", "base": 50000, "included": 30, "per_user": 1200},
}

3つのプランを、基本料金base・込みユーザー数included・超過1人あたりの単価per_userという共通の形で持たせています。料金改定のときはこの辞書の数値を書き換えるだけで、計算にも画面にも反映される作りです。

calculate関数に見積もりロジックを集約する

def calculate(plan_key: str, users: int, option_keys: list[str], billing: str) -> dict:
    plan = PLANS[plan_key]

金額に関わる計算はすべてこの関数に集めました。入力は文字列と数値だけ、出力は計算結果を詰めた辞書という素直な形なので、画面と切り離して単体でテストしやすくなります。

max関数で超過人数のマイナスを防ぐ

    extra_users = max(0, users - plan["included"])
    user_fee = extra_users * plan["per_user"]

利用人数がプランの込みユーザー数以下だとusers - plan["included"]は負の数になり、そのまま掛け算すると料金がマイナスになってしまいます。max(0, ...)で下限を0に固定するのが定番の守り方です。

リスト内包表記で選択オプションを取り出す

    selected_options = [OPTIONS[k] for k in option_keys if k in OPTIONS]
    option_fee = sum(o["price"] for o in selected_options)

クエリで受け取ったオプションのキーのうち、if k in OPTIONSでマスタに存在するものだけを拾います。URLに不正なキーを混ぜられても黙って無視されるだけなので、余計なエラー処理を書かずに済みました。

intの切り捨てで割引と消費税を計算する

    subtotal = plan["base"] + user_fee + option_fee
    discount = int(subtotal * YEARLY_DISCOUNT_RATE) if billing == "yearly" else 0
    tax = int((subtotal - discount) * TAX_RATE)

小計→年払いなら15%割引→残額に消費税10%、という決めた順序どおりの計算です。掛け算のたびにint()で円未満を切り捨てて整数に戻すことで、小数の誤差が金額に紛れ込むのを防いでいます。

render_pageで共通HTMLレイアウトを返す

def render_page(title: str, body: str) -> str:
    return f"""<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">

ページごとに違うのはタイトルと本文だけなので、共通の枠をf文字列で1回だけ書き、{title}{body}を差し込む方式にしました。テンプレートエンジンなしでも、この程度の規模なら十分に見通しよく書けます。

参考:

©FastAPI公式ドキュメント

To declare a query parameter with a type of list, like in the example above, you need to explicitly use Query, otherwise it would be interpreted as a request body.


Python基礎・実践(Django)

企業・法人向けのPython研修では、基礎から応用まで体系的に学べます。

Python研修の詳細

DX社員研修

企業・法人向けのDX研修では、実務に繋がるリスキリングでITレベルを向上させます。

DX研修の詳細

Javaエンジニア育成研修

企業・法人向けのJavaエンジニア育成研修では、Javaの基礎から応用まで確実に習得できます。

Java研修の詳細

新卒・新入社員向け研修

企業・法人に新入社員・新卒社員に向けたプログラミング研修を提供しています。

新入社員研修の詳細

コードキャンプのIT研修を全て見る

PythonのFastAPIコードをCodexにレビューさせた結果

プロンプトから作ったコードは、一見動いていても抜けや改善点が潜んでいることがあります。エラーにならない=正しい、ではないのが怖いところです。

そこで、コードを書いたClaude Codeとは別のAIであるCodexに同じコードをレビューさせ、指摘をそのまま載せます。書き手と読み手を別のAIに分けると、自分では気づきにくい問題を洗い出せます。

指摘 深刻度 改善案
billingにyearly以外の任意の値を指定しても月払いとして処理されるため、不正な入力を検出できません。 billingをLiteral['monthly','yearly']などで検証し、許可されない値には422エラーを返してください。
optionsに同じキーを複数回指定すると、同じオプション料金が重複加算されます。 オプションキーをsetで重複排除するか、重複入力をバリデーションエラーとして拒否してください。
存在しないオプションキーを指定しても黙って無視されるため、利用者の入力ミスに気付けません。 不正なオプションキーを検出して422エラーを返すか、結果画面に明示的な警告を表示してください。
usersが1未満または500超の場合にエラーではなく値を自動補正するため、入力値と見積もり条件が一致しません。 Queryのgeとleで範囲を検証し、範囲外の値は422エラーとして再入力を促してください。
年払いでも合計を月額合計と表示しており、15パーセント割引後の月額相当額なのか年間請求額なのかが不明確です。 年額を表示する仕様なら割引後の月額を12倍し、月額相当を表示する仕様なら見出しと説明を月額相当に統一してください。
さらに詳しく学べるPython研修の詳細はこちら

Pythonで4種類の見積もり画面を動かしてみた

開発サーバーを起動してブラウザからアクセスし、フォーム画面と条件違いの見積もり結果、合わせて4種類の画面を確認しました。いずれもHTTP 200で正常に表示されています。

結果画面は次の3パターンを試しました。プランと人数、オプションの絞り込み条件を変えるたびに、金額の内訳が組み替わって表示されるのを確認できています。

あわせて、ブラウザ操作の録画でも一連の流れをチェックしました。フォームで条件を選んで送信すると、URLのクエリパラメータが変わり、その条件どおりの見積もりに切り替わる様子が追えます。

このセクションの用語

uvicorn
FastAPIアプリを動かすためのサーバー。開発中はuvicorn main:app --reloadのように起動し、コードを保存すると自動で再読み込みされます。
HTTP 200
リクエストが正常に処理されたことを示すステータスコード。エラー時は404や422など別の番号が返ります。
  • スタンダードプラン・15人・優先サポート+自動バックアップ・年払い(/estimate?plan=standard&users=15&options=support&options=backup&billing=yearly)
  • ライトプラン・3人・オプションなし・月払い(/estimate?plan=light&users=3&billing=monthly)——込みユーザー数内に収まる最小構成
  • プレミアムプラン・40人・SSO連携・年払い(/estimate?plan=premium&users=40&options=sso&billing=yearly)——超過課金と割引が両方効く構成
画面: / (HTTP 200)
画面: / (HTTP 200)
画面: /estimate?plan=standard&users=15&options=support&options=backup&billing=yearly (HTTP 200)
画面: /estimate?plan=standard&users=15&options=support&options=backup&billing=yearly (HTTP 200)
画面: /estimate?plan=light&users=3&billing=monthly (HTTP 200)
画面: /estimate?plan=light&users=3&billing=monthly (HTTP 200)
画面: /estimate?plan=premium&users=40&options=sso&billing=yearly (HTTP 200)
画面: /estimate?plan=premium&users=40&options=sso&billing=yearly (HTTP 200)
アプリ操作中のブラウザ録画(実セッション・自動再生)
アプリ操作中のブラウザ録画(実セッション・自動再生)
さらに詳しく学べるPython研修の詳細はこちら

PythonとFastAPIの実行で起きやすいエラーと対処

この手の構成で初心者が遭遇しやすいエラーを、原因と対処のセットでまとめておきます。どれも一般によくあるパターンなので、動かないときはまずここから疑ってみてください。

特に4つ目のQueryまわりは、コードが一見正しく見えるだけに気づきにくいポイントです。GETでリストを受け取るときの約束事として覚えておくと、原因の切り分けが早くなります。

このセクションの用語

仮想環境
プロジェクトごとにライブラリを分けて管理するPythonの仕組み。有効化を忘れると、インストールしたはずのライブラリが見つからなくなります。
422 Unprocessable Entity
受け取った値が期待する型や条件に合わないときにFastAPIが自動で返すエラー。どの引数が問題かはレスポンスのdetailに書かれています。
エラー例 原因 対処
ModuleNotFoundError: No module named 'fastapi' ライブラリが未インストール、または別の仮想環境を見ている 仮想環境を有効化してからpip install "fastapi[standard]"を実行し直す
起動時にaddress already in useと表示される ポート8000を別のプロセスがすでに使っている uvicorn main:app --port 8001のように空いているポート番号を指定する
画面に{"detail":[{"type":"int_parsing",...}]}(422)が出る usersなど整数の引数に数値でない値が渡っている フォームの入力欄にtype="number"を指定し、URL直打ちのときは整数を渡す
optionsを複数付けてもリストで受け取れない Queryを明示せずにリスト型の引数を宣言し、リクエストボディ扱いになっている options: list[str] = Query(default=[])の形で宣言する
ブラウザで日本語が文字化けする HTMLに文字コードの指定がない <meta charset="utf-8"><head>内に入れる
さらに詳しく学べるPython研修の詳細はこちら

クエリパラメータ設計でつまずきやすいポイント

フォームからGETで値を渡す今回の構成は仕組みが単純なぶん、名前や順序のちょっとしたズレが「値が届かない」「金額が合わない」という分かりにくい不具合につながります。ハマりやすい点を先にまとめておきます。

このセクションの用語

name属性
フォームの入力部品に付ける名前。送信時にはこの名前がクエリパラメータのキーになるため、受け取る側の引数名と揃える必要があります。
浮動小数点数
小数を近似値で扱う数値型。0.1のような値も2進数では正確に表せないため、金額計算では誤差の原因になりえます。
POINT

HTMLフォームのname属性は、エンドポイント側の引数名と完全に一致させる必要があります。1文字でも違うと値が渡らず、デフォルト値のまま計算されてしまうのが厄介です。

チェックボックスは全部同じname="options"にすると、options=support&options=backupのような複数値として送信されます。1つずつ別の名前を付けるとリストで受け取れなくなるので注意してください。

お金の計算に浮動小数点数をそのまま使うと、微小な誤差が合計に紛れ込むことがあります。今回は掛け算のたびにint()で円未満を切り捨て、常に整数で持つ方針に統一しました。

小計→割引→税の適用順を入れ替えると、同じ入力でも合計金額が変わります。どの順で丸めるかも含めて、計算ルールは実装前に仕様として固定しておくのが安全です。

さらに詳しく学べるPython研修の詳細はこちら

料金シミュレーターが実務で使える場面

「基本料金+人数課金+オプション+割引」という構造は、SaaSに限らず多くの料金体系に共通しています。PLANSOPTIONSの中身を差し替えるだけで流用できるので、応用の幅は広いはずです。

実務での使いどころを、具体的な使い方まで含めて挙げてみます。

使える場面 具体的な使い方
SaaS製品の営業・商談支援 商談中にプラン・人数・オプションをその場で切り替えて税込金額を提示する。料金改定時はPLANS辞書の数値を書き換えるだけで画面にも反映される
社内システムの部署別課金の案内 情シスが利用部署ごとの人数とオプションを入力して概算を出し、結果画面のURLをそのまま稟議資料や依頼メールに貼って共有する
代理店・パートナー向け見積もりツール 計算ロジックがcalculate関数に一本化されているため、割引率の定数だけを代理店ごとに変えた派生版を短時間で用意できる
ジムやレンタルスペースなど非IT業種の料金案内 「基本料+超過課金+オプション」という構造は共通なので、PLANSの項目名と単価を差し替えて店頭タブレット用の料金案内として使う
さらに詳しく学べるPython研修の詳細はこちら

Pythonの1ファイル構成で作ってみたまとめ

FastAPIのGETエンドポイント2つと辞書の料金マスタだけで、料金プラン見積もりシミュレーターを1ファイルに収めました。実際に4種類の画面で表示と絞り込みの動作を確認でき、フォームで条件を変えると内訳ごと金額が切り替わるところまで動いています。

振り返ると、学びの中心は次の3点でした。どれもFastAPIに限らず、Webアプリで「条件を受け取って計算結果を返す」場面全般で効いてくる考え方です。

次のステップとしては、Queryge=1のような値の範囲チェックを足したり、料金マスタをJSONファイルに切り出したりする拡張が考えられます。まずはこの1ファイルを手元で動かして、PLANSの数値を自分の題材に書き換えるところから試してみてください。

  • 複数選択は同名クエリパラメータの繰り返しで表現し、Query(default=[])でリストとして受け取る
  • お金の計算は「小計→割引→税、円未満切り捨て」のように、適用順と丸め方を先に仕様として決めてから実装する
  • 計算をcalculate関数、表示をrender_page関数に分けておくと、1ファイル構成でも見通しが保てる
さらに詳しく学べるPython研修の詳細はこちら

参考にした一次情報

  1. ^ FastAPI公式ドキュメント(日本語). https://fastapi.tiangolo.com/ja/, (参照26-07-17).
  2. ^ クエリパラメータ - FastAPI. https://fastapi.tiangolo.com/ja/tutorial/query-params/, (参照26-07-17).
  3. ^ カスタムレスポンス(HTMLResponse) - FastAPI. https://fastapi.tiangolo.com/ja/advanced/custom-response/, (参照26-07-17).
  4. ^ Uvicorn公式ドキュメント. https://www.uvicorn.org/, (参照26-07-17).

※内容は執筆時点のものです。ライブラリやサイトの仕様は変わる可能性があるため、公式ドキュメントもあわせてご確認ください。

ITやプログラミングに関するコラム


ITやプログラミングに関するニュース

ブログに戻る

コメントを残す

コメントは公開前に承認される必要があることにご注意ください。

企業・法人向けのIT・プログラミング・生成AI研修を探す、比較する - IT・プログラミングを知って学べるコネクトメディア CodeCampが提供するDX人材育成が可能なプログラミングやITが学べる公開講座 - IT・プログラミングを知って学べるコネクトメディア コードキャンプが提供する無料で学べるプログラミングスクール講座 - IT・プログラミングを知って学べるコネクトメディア コードキャンプDX人材育成研修 - IT・プログラミングを知って学べるコネクトメディア 3.5日の研修で、年間1,600時間の削減効果が見込まれる。東京きらぼしフィナンシャルグループのDX人材育成事例 - IT・プログラミングを知って学べるコネクトメディア 配属3ヶ月で30%の生産性向上を実現するいよぎんコンピュータサービスの新人研修に迫る - IT・プログラミングを知って学べるコネクトメディア 金融業界の業務効率化を加速するニッセイアセットマネジメントの生成AI×GAS活用研修事例 - IT・プログラミングを知って学べるコネクトメディア 【製造業のDX人材育成事例】デジタル人材の即戦力化を実現する、日本ガイシ株式会社の異動者向オンボーディング研修 - ITやプログラミングを知って学べるコネクトメディア フューチャーアーキテクト株式会社が実現した新入社員向けIT研修プログラムでタスクフォース制度が主体的な学びと成長を生み出す - IT・プログラミングを知って学べるコネクトメディア コードキャンプDX人材育成研修 - IT・プログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/【IT新入社員研修】オンラインとオフラインの最適バランスを実現したFutureOneの導入事例 - IT・プログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/【新入社員研修】柔軟なハイブリッド型Java研修で実現した新卒20名の成長と成果|サークレイス株式会社 - ITやプログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/現場により近いところにデジタルを根付かせるDX基礎講座研修|株式会社ブリヂストン - ITやプログラミングを知って学べるコネクトメディア コードキャンプIT・プログラミング研修事例/業務の効率化・DX推進に向けたIT人材育成への第一歩|株式会社カナエ - ITやプログラミングを知って学べるコネクトメディア 企業・法人向けのIT・プログラミング研修 - ITやプログラミングを知って学べるコネクトメディア

新着記事

対象者別で探す

子供(小学生・中学生・高校生)向け
プログラミング教室検索する

子供(小学生・中学生・高校生)がロボットやプログラミング言語を学ぶことができるオフラインからオンラインスクールを検索、比較することが可能です。

子供(小学生・中学生・高校生)
プログラミング教室検索する

ITやプログラムなどの
最新情報を検索する

日々、新しいITやプログラミング言語の情報が流れていきますが、特定の情報を時系列でニュースやコラムを確認することができます。

ITやプログラムなどの
最新情報を検索する