はじめに
Day 4では、タスク管理機能のデータ構造についてJSONLとUUIDの選択理由を書きました。
今回はFastAPIをAPIハブとして設計した考え方について書きます。フロントエンドとバックエンドの責務分離をどのように実現しているか、インスタンス化の方針とエンドポイント設計の判断を記録します。
FastAPIをAPIハブとして使う
今回のツールではFastAPIを使用し、バックエンドとフロントエンドを繋いでいます。
FastAPIの選定理由はDay 2で書きましたが、APIハブとして採用した実践的な理由はもう一つあります。今回のツールは配布を前提としており、バイナリファイルをできるだけ小さく・1つにまとめることに重点を置いています。NN機能を搭載しながら軽量に仕上げるという目的のため、必要な機能のみに絞った実装が求められます。FastAPIはその軽量さと実装しやすさの両立という点で、この方針に適していると判断しました。
全体の構成は以下の通りです。
import fastapi
import fastapi.middleware.cors
import src.back.goals as goals, src.back.times as times
import src.back.file_operations as files
from typing import Annotated
app = fastapi.FastAPI()
timer = times.TimeCheker()
app.add_middleware(
fastapi.middleware.cors.CORSMiddleware,
allow_origins=["http://localhost:5173"]
)
@app.get("/timer/start")
def timer_start():
result = timer.start_checker()
return result
@app.post("/timer/stop")
def timer_end():
result = timer.end_checker()
return result
@app.get("/timer/today")
def today_timer():
result = timer.import_to_csv()
return result
@app.post("/goals/save")
def goals_set(goal:Annotated[list[str], fastapi.Form()], month):
result = goals.Goals(goal, status=None, limit=None,month=month).save()
return result
@app.post("/goals/update")
def goals_update(key, status, limit, month):
result = goals.Goals(key=key, status=status, limit=limit, month=month).update()
return result
@app.get('/data/json')
def get_to_jsonl(month, year):
result = files.FileSearch(month=month, year=year).reard_to_jsonl()
return result
@app.get('/data/csv')
def get_to_csv(month, year):
result = files.FileSearch(month=month, year=year).reard_to_csv()
return result
インスタンス化の方針
実装においてtimer機能は永続インスタンスとして保持し、goals機能はリクエストごとにインスタンス化しています。機能ごとに目的が異なるため、同じ方法を取っていません。
コードを見ると、timerはモジュールレベルで1度だけインスタンス化されているのに対し、goalsはエンドポイントの関数内でその都度インスタンス化されていることが確認できます。
timerを永続インスタンスにした理由
timerは開始時刻を記録し、終了時刻と合わせて合計時間を算出する必要があります。startとstopで共通のflagと開始時刻を参照しなければならないため、リクエストをまたいで同一インスタンスを保持する必要があります。
goalsをリクエストごとにインスタンス化した理由
goalsは機能ごとに受け取る引数が異なります。登録・更新でそれぞれ異なるパラメータを受け取るため、共通のインスタンスを使い回すと必要な情報が受け取れず機能不全を起こします。また、goalsが参照するのはJSONLという静的ファイルであり、一度記録されてしまえば同じインスタンスを参照し続ける必要がありません。これらの理由から、各リクエストごとに必要な引数を渡してインスタンス化する方針を取っています。
タスクの受け取り方法
フロントからタスクを受け取る際の型にはAnnotated[list[str], fastapi.Form()]を指定しています。
本ツールではタスクの登録を同時に4件まで想定しているためです。最初はstr型で実験しましたが、複数タスクを受け取る性質上リスト形式での受け渡しが必須であることに気づき、フォームの内容をそのまま受け取れるAnnotated[list[str], fastapi.Form()]を採用しました。
エンドポイントの設計判断
エンドポイントには以下の命名規則を設けています。
| エンドポイント | 対象機能 |
|---|---|
/timer/start /timer/stop /timer/today
|
timer機能 |
/goals/save /goals/update
|
goals機能 |
/data/json /data/csv
|
データ取り出し機能 |
この設計はフロントエンドの画面構成に由来しています。フロントでは共通コンポーネントと機能コンポーネントに分けた設計を採用しており、機能コンポーネントはユーザーの選択に応じて切り替わる仕様です。エンドポイントを機能単位で明確に分けることで、各機能コンポーネントが呼び出すAPIに誤りがないかをすぐに確認できます。フロントの設計については次のDay 6で詳しく書く予定です。
おわりに
今日はFastAPIによるAPIハブの設計について書きました。
永続インスタンスとリクエストごとのインスタンス化を使い分けたのは、それぞれの機能が持つ「状態を保持する必要があるか」という観点からの判断です。設計の理由を言語化しておくと、後から見返したときに判断の根拠が残せます。
Day 6では、TypeScriptによるクラスベースのコンポーネント設計について書く予定です。Reactを使わずにクラスで責務を分けた設計の判断と、実装中にハマったポイントを記録します。
リポジトリはOSS公開準備中です。公開後にこの記事へリンクを追加します。
この記事は連載「クラウドに依存しないマイルストーン管理ツール開発記」のDay 5です。
- Day 1 - なぜ自作するのか
- Day 2 - 技術スタックとその選定理由
- Day 3 - Pythonで時間記録機能を作る
- Day 4 - タスク管理のデータ構造
- Day 6 - TypeScriptでクラスベースのコンポーネント設計(予定)