【Python】SQLModel・FastAPI・SQLiteで読書記録アプリを作ってみた

【Python】SQLModel・FastAPI・SQLiteで読書記録アプリを作ってみた

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

SQLModelとFastAPI、SQLiteを使って、読んだ本の書名・著者・読了日・評価を管理する小さな読書記録APIを作ります。モデル定義から4つのAPI、ブラウザで動く画面までを1ファイルにまとめ、初心者でも追体験できるように順を追って解説します。

SQLModelの基本概念、要件定義、実装、動作確認までを順番に学べる構成です。動画は目次から確認したい場面へ移動でき、本文だけでも手順と考え方が完結します。

【Python】SQLModel・FastAPI・SQLiteで読書記録アプリを作ってみた|フルカリキュラム
動画の内容をテキストで確認する

オープニング。SQLModelとFastAPIを使って読書記録アプリを作るカリキュラムを始めます。概要紹介。

SQLModelとFastAPIの役割と使い方を学ぶ 読書記録アプリの要件を整理する 完成コードと実行結果を確認する 最後に完成した画面を実際に操作して確かめる 具体的にやること。

書名と著者を送って書籍を登録する機能 登録済みの書籍をid順で一覧取得する機能 書籍IDを指定して読了日を記録する機能 書籍IDを指定して1〜5の評価を記録する機能 評価を★の数で表示し未評価を区別する画面 実装環境・必須アプリ。

OS:Windows 11 Pro Python:3.13.3シェル:PowerShell 5.1必須アプリ:コードエディター、Webブラウザ、ターミナル パッケージ:pip、"fastapi==0.115.6"、"sqlmodel==0.0.22"、"uvicorn[standard]==0.30.6"、"pysqlite3-binary==0.5.3" SQLModelとFastAPIとは。

SQLModel:SQLModelは、Pythonのクラス定義をそのままデータベースのテーブルとして扱えるORMライブラリFastAPI:FastAPIは、PythonでWeb APIを短いコードで構築できるWebフレームワーク SQLModelとFastAPIで作る読書記録アプリの要点。

登録ボタンでPOSTを送り一覧を再取得 更新ボタンで読了日と評価をまとめて送信 空欄の項目は送らず既存の値を保持 読書記録アプリの要件定義。

POSTした書名と著者が登録済み書籍として返るGET /api/booksで登録した本がid順に返るPATCH /finishedで読了日が保存されるPATCH /ratingで1〜5の評価が保存される 範囲外の評価に400エラーが返る 存在しないIDに404エラーが返る INTRO: Monaco Editorで読書記録アプリを実装。

コードを1行ずつ入力し、補完と自動インデントを使いながら実行結果を確認します。LINE 001: 日付型のインポート。標準ライブラリのdatetimeからdateをインポートしています。

読了日など日付だけを扱う項目の型として、このあと利用します。LINE 002: Optional型のインポート。typingからOptionalをインポートしています。

値が入る場合と入らない場合がある項目の型を表すために使います。LINE 004: FastAPI本体の読み込み。fastapiからFastAPIクラスとHTTPExceptionをインポートしています。

FastAPIはアプリ本体を作るため、HTTPExceptionはエラー応答を返すために使います。LINE 005: HTML応答用クラスの読み込み。fastapi.responsesからHTMLResponseをインポートしています。

APIの応答としてHTML画面を返す際に利用します。LINE 006: BaseModelの読み込み。pydanticからBaseModelをインポートしています。

リクエストで受け取るデータの形を定義するための基底クラスです。LINE 007: StaticPoolの読み込み。sqlalchemy.poolからStaticPoolをインポートしています。

SQLiteのメモリ内データベースを使う際に接続を共有するために使います。LINE 008: SQLModel関連の読み込み。sqlmodelからField、Session、SQLModel、create_engine、selectをインポートしています。

データベースのモデル定義や操作に必要な部品をまとめて取り込んでいます。LINE 012: 書籍テーブルの定義開始。SQLModelを継承しtable=Trueを指定することで、書籍情報を保存するデータベーステーブルとしてBookクラスを定義しています。

LINE 013: 主キーidの定義。idフィールドをOptionalなint型で定義し、primary_key=Trueで主キーに設定しています。登録時は指定しなくても自動で採番されます。

LINE 014: 書名フィールドの定義。titleフィールドをstr型で定義しています。書籍のタイトルを必ず保存するための項目です。

LINE 015: 著者フィールドの定義。authorフィールドをstr型で定義しています。書籍の著者名を必ず保存するための項目です。

LINE 016: 読了日フィールドの定義。finished_onをOptionalなdate型で定義し、初期値をNoneにしています。まだ読み終えていない本は空のままにできます。

LINE 017: 評価フィールドの定義。ratingをOptionalなint型で定義し、初期値をNoneにしています。評価をつけるまでは空のままにできます。

RUN 1/9: Bookモデルの列を確認する。SQLModelで定義したBookモデルに、想定した列がそろっているかを確認します。CHECK 1/9: 途中実行に成功。

['id', 'title', 'author', 'finished_on', 'rating'] RETURN 01: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 021: 登録用スキーマの定義開始。

BaseModelを継承してBookCreateクラスを定義しています。書籍を新規登録する際にクライアントから受け取るデータの形を表します。LINE 022: 登録時の書名項目。

titleフィールドをstr型で定義しています。新規登録リクエストに必ず書名が含まれることを表します。LINE 023: 登録時の著者項目。

authorフィールドをstr型で定義しています。新規登録リクエストに必ず著者名が含まれることを表します。LINE 026: 読了日更新用スキーマの定義。

BaseModelを継承してFinishedUpdateクラスを定義しています。読了日を更新するリクエストの形を表します。LINE 027: 読了日項目の定義。

finished_onフィールドをdate型で定義しています。更新リクエストには必ず読了日が含まれることを表します。LINE 030: 評価更新用スキーマの定義。

BaseModelを継承してRatingUpdateクラスを定義しています。評価を更新するリクエストの形を表します。LINE 031: 評価項目の定義。

ratingフィールドをint型で定義しています。更新リクエストには必ず評価の数値が含まれることを表します。RUN 2/9: リクエスト用スキーマを確認する。

登録と更新で受け取る項目が、用途ごとに分かれていることを確認します。CHECK 2/9: 途中実行に成功。['title', 'author'] ['finished_on'] ['rating'] RETURN 02: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 035: データベースエンジンの作成開始。create_engine関数を呼び出し、データベースへの接続設定であるengineを作成しています。

この設定をもとにセッションが作られます。LINE 036: SQLite接続先の指定。接続文字列として"sqlite://"を指定しています。

ファイルを使わないメモリ上のSQLiteデータベースとして動作します。LINE 037: スレッドチェックの無効化。connect_argsでcheck_same_thread=Falseを指定しています。

複数のリクエストから同じ接続を扱えるようにするための設定です。LINE 038: コネクションプールの指定。poolclassにStaticPoolを指定しています。

メモリ内データベースの内容がリクエストごとに消えないよう、接続を使い回す設定です。LINE 039: create_engine呼び出しの終了。create_engine関数への引数指定を閉じています。

ここまでの設定でengine変数が作成されます。LINE 040: テーブルの作成実行。SQLModel.metadata.create_all(engine)を呼び出し、定義したBookモデルに対応するテーブルを実際のデータベース上に作成しています。

RUN 3/9: データベースにテーブルが作られたか確認する。create_allの実行後に、SQLiteへbookテーブルが用意されたことを確認します。CHECK 3/9: 途中実行に成功。

['book'] RETURN 03: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 044: FastAPIアプリの生成。

FastAPIクラスをインスタンス化し、タイトルを「読書記録アプリ」としてapp変数に代入しています。このapp変数に各APIの処理を登録していきます。RUN 4/9: FastAPIアプリの生成を確認する。

アプリのタイトルが設定され、FastAPIのインスタンスが作られたことを確認します。CHECK 4/9: 途中実行に成功。読書記録アプリ RETURN 04: エディターへ戻りました。

入力済みのコードを維持したまま、次の実装へ進みます。LINE 048: 書籍登録APIのルート定義。POSTメソッドで"/api/books"にアクセスされたときに、このあとの関数を実行するよう設定しています。

LINE 049: 書籍登録関数の定義。create_book関数を定義し、引数payloadでBookCreate形式のリクエストデータを受け取ります。LINE 050: Bookインスタンスの作成。

受け取ったpayloadのtitleとauthorを使って、新しいBookオブジェクトを作成しています。まだデータベースには保存されていません。LINE 051: データベースセッションの開始。

withを使ってSession(engine)を開き、この範囲内でデータベースへの操作を行います。処理が終われば自動的にセッションが閉じられます。LINE 052: 書籍の登録予約。

session.addでbookをセッションに追加しています。この時点ではまだデータベースに書き込まれず、変更の予約状態です。LINE 053: 変更の確定。

session.commitを呼び出し、追加した書籍情報を実際にデータベースへ保存しています。LINE 054: 最新情報の取得。session.refreshでbookの情報をデータベースの最新状態に更新しています。

自動採番されたidなどがここで反映されます。LINE 055: 登録結果の返却。更新済みのbookをレスポンスとして返しています。

呼び出し元にはidを含む登録済みの書籍情報が返ります。RUN 5/9: 登録APIの動作を確認する。書名と著者を送って書籍が登録され、idが割り振られることを確認します。

CHECK 5/9: 途中実行に成功。{'rating': None, 'author': '宮沢賢治', 'finished_on': None, 'id': 1, 'title': '銀河鉄道の夜'} RETURN 05: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。

LINE 059: 読了日更新APIのルート定義。PATCHメソッドで"/api/books/{book_id}/finished"にアクセスされたときに、このあとの関数を実行するよう設定しています。LINE 060: 読了日更新関数の定義。

update_finished関数を定義し、URLからbook_idを、リクエストボディからFinishedUpdate形式のpayloadを受け取ります。LINE 061: データベースセッションの開始。withを使ってSession(engine)を開き、この範囲内で書籍の検索や更新の処理を行います。

LINE 062: 対象書籍の取得。session.getでBookテーブルからbook_idに一致する書籍を取得しています。見つからない場合はNoneが返ります。

LINE 063: 存在チェックの開始。取得したbookがNoneかどうかを判定しています。書籍が見つからなかった場合の処理に分岐します。

LINE 064: 404エラーの送出。bookが見つからない場合にHTTPExceptionを発生させ、ステータスコード404とエラーメッセージをクライアントへ返しています。LINE 065: 読了日の更新。

取得したbookのfinished_onにpayloadの読了日を代入し、書籍情報を更新しています。LINE 066: 更新内容の登録予約。session.addでbookをセッションに追加し、更新内容をデータベースへ反映する準備をしています。

LINE 067: 変更の確定。session.commitを呼び出し、読了日の更新内容を実際にデータベースへ保存しています。LINE 068: 最新情報の取得。

session.refreshでbookの情報をデータベースの最新状態に更新しています。LINE 069: 更新結果の返却。更新済みのbookをレスポンスとして返しています。

呼び出し元には読了日が反映された書籍情報が返ります。RUN 6/9: 読了日の更新APIを確認する。登録済みの本に読了日を追記し、値が保存されることを確認します。

CHECK 6/9: 途中実行に成功。{'author': '宮沢賢治', 'finished_on': '2026-07-31', 'id': 1, 'title': '注文の多い料理店', 'rating': None} RETURN 06: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。

LINE 073: 評価更新APIのルート定義。PATCHメソッドで"/api/books/{book_id}/rating"にアクセスされたときに、このあとの関数を実行するよう設定しています。LINE 074: 評価更新関数の定義。

update_rating関数を定義し、URLからbook_idを、リクエストボディからRatingUpdate形式のpayloadを受け取ります。LINE 075: 評価範囲のチェック。payload.ratingが1から5の範囲に収まっているかどうかを判定しています。

範囲外の場合はこのあとエラーになります。LINE 076: 400エラーの送出。評価が1〜5の範囲外だった場合にHTTPExceptionを発生させ、ステータスコード400とエラーメッセージをクライアントへ返しています。

LINE 077: データベースセッションの開始。withを使ってSession(engine)を開き、この範囲内で書籍の検索や更新の処理を行います。LINE 078: 対象書籍の取得。

session.getでBookテーブルからbook_idに一致する書籍を取得しています。見つからない場合はNoneが返ります。LINE 079: 存在チェックの開始。

取得したbookがNoneかどうかを判定しています。書籍が見つからなかった場合の処理に分岐します。LINE 080: 404エラーの送出。

bookが見つからない場合にHTTPExceptionを発生させ、ステータスコード404とエラーメッセージをクライアントへ返しています。LINE 081: 評価の更新。取得したbookのratingにpayloadの評価値を代入し、書籍情報を更新しています。

LINE 082: 更新内容の登録予約。session.addでbookをセッションに追加し、更新内容をデータベースへ反映する準備をしています。LINE 083: 変更の確定。

session.commitを呼び出し、評価の更新内容を実際にデータベースへ保存しています。LINE 084: 最新情報の取得。session.refreshでbookの情報をデータベースの最新状態に更新しています。

LINE 085: 更新結果の返却。更新済みのbookをレスポンスとして返しています。呼び出し元には評価が反映された書籍情報が返ります。

RUN 7/9: 評価の更新APIを確認する。登録済みの本に1〜5の評価を追記し、値が保存されることを確認します。CHECK 7/9: 途中実行に成功。

{'author': '宮沢賢治', 'finished_on': None, 'id': 1, 'title': '風の又三郎', 'rating': 4} RETURN 07: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 089: 一覧取得APIのルート定義。

GETメソッドで"/api/books"にアクセスされたときに、このあとの関数を実行するよう設定しています。LINE 090: 一覧取得関数の定義。list_books関数を定義しています。

登録済みの書籍を一覧として返すための処理をこのあと行います。LINE 091: データベースセッションの開始。withを使ってSession(engine)を開き、この範囲内で書籍一覧の検索処理を行います。

LINE 092: 書籍一覧の取得と返却。select(Book)にBook.idでの並び替えを組み合わせてsession.execで実行し、結果をallで取り出して返しています。id順に並んだ全書籍のリストがレスポンスになります。

RUN 8/9: 一覧取得APIを確認する。登録した本がid順に並んで取得できることを確認します。CHECK 8/9: 途中実行に成功。

[{'author': '宮沢賢治', 'finished_on': None, 'id': 1, 'title': 'セロ弾きのゴーシュ', 'rating': None}] RETURN 08: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。LINE 096: 画面用HTML文字列の定義開始。

INDEX_HTML変数に複数行文字列でHTMLの内容を代入し始めています。この文字列がトップページの画面として利用されます。LINE 097: HTML文書の開始とlang属性。

HTML文書全体の開始タグです。lang属性に"ja"を指定することで、このページが日本語のコンテンツであることをブラウザに伝えています。LINE 098: headセクションの開始。

ページのメタ情報を記述するhead要素の開始タグです。ここには文字コードやタイトル、スタイルなど画面には直接表示されない設定を書いていきます。LINE 099: 文字コードの指定。

ページの文字コードをUTF-8に指定しています。日本語などの文字が正しく表示されるようにするための設定です。LINE 100: ブラウザタブのタイトル。

ブラウザのタブに表示されるページタイトルを「読書記録アプリ」に設定しています。LINE 101: styleタグの開始。ページ全体の見た目を定義するCSSの記述を始めるタグです。

この中にデザインのルールをまとめて書いています。LINE 102: body全体のスタイル設定。ページ全体のフォントや余白、最大幅を指定しています。

文字を読みやすいフォントにし、画面が広がりすぎないように幅を制限しています。LINE 103: section要素の見た目設定。各セクションを枠で囲むためのスタイルです。

境界線と角丸、内側の余白、下側の余白を指定して見やすく区切っています。LINE 104: input要素の余白設定。入力欄の内側と外側の余白を指定しています。

入力欄同士が詰まりすぎないように調整しています。LINE 105: button要素の見た目設定。ボタンの内側の余白とマウスカーソルの形を指定しています。

カーソルを合わせるとクリックできることが分かるようにしています。LINE 106: リスト項目の余白設定。書籍一覧のli要素に上下の余白を付けて、項目同士が読みやすく並ぶようにしています。

LINE 107: styleタグの終了。CSSの記述を終えるタグです。ここまでの内容でページ全体の見た目のルールが決まります。

LINE 108: headセクションの終了。head要素の終了タグです。ここまででページのメタ情報とスタイルの定義が完了します。

LINE 109: bodyセクションの開始。画面に実際に表示される内容を記述するbody要素の開始タグです。ここから見出しや入力欄などの画面部品を書いていきます。

LINE 110: ページの見出し。画面の一番上に表示される大見出しです。「読書記録アプリ」というタイトルをユーザーに示しています。

LINE 112: 登録セクションの開始。書籍を新規登録するための入力欄をまとめるセクションです。idにregisterを指定して他の部分と区別しています。

LINE 113: 登録セクションの小見出し。このセクションが「書籍を登録」する機能であることを示す小見出しです。LINE 114: 書名入力欄。

書籍のタイトルを入力するためのテキストボックスです。idをbook-titleとして、あとでJavaScriptから値を取得できるようにしています。LINE 115: 著者入力欄。

書籍の著者名を入力するためのテキストボックスです。idをbook-authorとして、後続の処理で値を参照できるようにしています。LINE 116: 登録ボタン。

入力した書名と著者を登録するためのボタンです。idをadd-bookとして、クリック時の処理と結び付けられるようにしています。LINE 117: 登録セクションの終了。

書籍登録用のセクションを閉じるタグです。ここまでで登録フォームの表示部分が完成します。LINE 119: 管理セクションの開始。

読了日と評価を更新するための入力欄をまとめるセクションです。idにmanageを指定しています。LINE 120: 管理セクションの小見出し。

このセクションが「読了日と評価を管理」する機能であることを示す小見出しです。LINE 121: 対象書籍ID入力欄。更新したい書籍のIDを入力する欄です。

初期値として1が入っており、idをtarget-idとして値を取得できるようにしています。LINE 122: 読了日入力欄。読了した日付を入力する欄です。

プレースホルダーで入力形式(YYYY-MM-DD)を案内し、idをfinished-inputとしています。LINE 123: 評価入力欄。書籍の評価を1から5の数値で入力する欄です。

type属性をnumberにし、min・maxで範囲を1〜5に制限しています。LINE 124: 更新ボタン。入力した読了日や評価をサーバーに送信して更新するためのボタンです。

idをsave-progressとしています。LINE 125: 管理セクションの終了。読了日と評価の管理用セクションを閉じるタグです。

ここまでで更新フォームの表示部分が完成します。LINE 127: 一覧セクションの開始。登録済みの書籍を一覧表示するためのセクションです。

idにlibraryを指定しています。LINE 128: 一覧セクションの小見出し。このセクションが「登録済みの本」を表示する機能であることを示す小見出しです。

LINE 129: 書籍一覧のリスト枠。取得した書籍データを表示するための空のul要素です。idをbook-listとして、JavaScriptから中身を書き換えられるようにしています。

LINE 130: 一覧セクションの終了。書籍一覧用のセクションを閉じるタグです。ここまでで一覧表示部分の枠組みが完成します。

LINE 132: scriptタグの開始。ここから画面の動きを制御するJavaScriptのコードを記述していきます。ボタンの操作やデータ取得の処理をこの中に書いています。

LINE 133: loadBooks関数の定義開始。書籍一覧を取得して画面に表示するための非同期関数を定義しています。この関数は複数の場所から呼び出されて一覧を最新の状態に更新します。

LINE 134: 書籍一覧APIの呼び出し。サーバーの/api/booksエンドポイントにリクエストを送り、登録されている書籍のデータを取得しています。LINE 135: レスポンスのJSON変換。

サーバーから返ってきたレスポンスをJSON形式に変換し、書籍データの配列として扱えるようにしています。LINE 136: リスト要素の取得。画面上のbook-listというidを持つul要素を取得し、あとでその中身を書き換えられるようにしています。

LINE 137: リスト内容のクリア。一覧を表示する前に、既存のリスト内容を空にしています。これにより古い表示が残らず、最新のデータだけが表示されます。

LINE 138: 書籍データのループ処理。取得した書籍データの配列を1件ずつ取り出して処理するための繰り返し処理です。この中で各書籍の表示用要素を作成します。

LINE 139: リスト項目要素の作成。1件分の書籍情報を表示するためのli要素を新しく作成しています。LINE 140: 評価の星表示への変換。

評価が設定されていれば評価の数だけ星マークを繰り返し表示し、未設定の場合は「未評価」と表示するようにしています。LINE 141: 読了日の表示切り替え。読了日が設定されていればその日付を表示し、設定されていなければ「未読了」と表示するようにしています。

LINE 142: 書籍情報のテキスト作成(1行目)。書籍のIDとタイトル、著者を組み合わせて表示用の文字列の前半部分を作成しています。LINE 143: 書籍情報のテキスト作成(2行目)。

先ほどの文字列に読了日と評価の情報を続けて連結し、li要素のテキストとして設定しています。LINE 144: リストへの要素追加。作成したli要素を書籍一覧のul要素に追加し、画面上に1件分の書籍情報を表示させています。

LINE 145: ループ処理の終了。書籍データを1件ずつ処理する繰り返し処理を終える閉じ括弧です。ここまでですべての書籍が一覧に反映されます。

LINE 146: loadBooks関数の定義終了。書籍一覧を取得して表示するloadBooks関数の定義を終える閉じ括弧です。LINE 148: 登録ボタンのクリック処理開始。

add-bookボタンがクリックされたときに実行される非同期処理を登録しています。この中で入力内容の取得と登録リクエストを行います。LINE 149: 書名の取得。

書名の入力欄から値を取得し、前後の余分な空白を取り除いています。LINE 150: 著者名の取得。著者名の入力欄から値を取得し、前後の余分な空白を取り除いています。

LINE 151: 未入力チェック。書名か著者名のどちらかが空である場合は、以降の登録処理を行わずに関数の実行を終了させています。LINE 152: 書籍登録APIの呼び出し開始。

サーバーの/api/booksエンドポイントに対して、新しい書籍を登録するリクエストを送る処理を開始しています。LINE 153: HTTPメソッドの指定。リクエストの種類としてPOSTメソッドを指定し、新しいデータを送信することをサーバーに伝えています。

LINE 154: リクエストヘッダーの指定。送信するデータの形式がJSONであることをサーバーに伝えるためのヘッダーを設定しています。LINE 155: 送信データの作成。

入力された書名と著者名をJSON形式の文字列に変換し、リクエストの本文として設定しています。LINE 156: 登録リクエストの終了。書籍登録のためのfetch呼び出しの設定を締めくくる部分です。

LINE 157: 書名入力欄のリセット。登録処理が完了した後、書名の入力欄を空にして次の入力に備えています。LINE 158: 著者入力欄のリセット。

登録処理が完了した後、著者名の入力欄を空にして次の入力に備えています。LINE 159: 一覧の再読み込み。新しい書籍を登録したあとに、loadBooks関数を呼び出して一覧表示を最新の状態に更新しています。

LINE 160: クリック処理の登録終了。登録ボタンのクリック時処理を定義するaddEventListenerの呼び出しを締めくくる部分です。LINE 162: 更新ボタンのクリック処理開始。

save-progressボタンがクリックされたときに実行される非同期処理を登録しています。この中で読了日と評価の更新リクエストを行います。LINE 163: 対象書籍IDの取得。

更新対象となる書籍のIDを入力欄から取得し、前後の余分な空白を取り除いています。LINE 164: ID未入力チェック。書籍IDが入力されていない場合は、以降の更新処理を行わずに関数の実行を終了させています。

LINE 165: 読了日の取得。読了日の入力欄から値を取得し、前後の余分な空白を取り除いています。LINE 166: 評価の取得。

評価の入力欄から値を取得し、前後の余分な空白を取り除いています。LINE 167: 読了日入力の有無を判定。読了日の入力欄に値が入っているかどうかを確認しています。

値がある場合だけ読了日を更新する処理を実行します。LINE 168: 読了日更新APIを呼び出し。入力された書籍IDを使って、読了日更新用のAPIエンドポイントへリクエストを送信しています。

URLの一部に書籍IDを埋め込んでいます。LINE 169: HTTPメソッドをPATCHに指定。サーバーへ送るリクエストの種類をPATCHに設定しています。

既存データの一部だけを更新する際に使うメソッドです。LINE 170: 送信データの形式を指定。リクエストのヘッダーで、送信するデータがJSON形式であることをサーバーに伝えています。

LINE 171: 読了日をJSON形式に変換。入力された読了日の値をJSON文字列に変換し、リクエストの本文として設定しています。LINE 172: fetch呼び出しの終了。

読了日更新のためのfetch処理の記述を閉じています。ここまでの設定内容でリクエストが送信されます。LINE 173: if文の終了。

読了日が入力されている場合の処理をここで終了しています。LINE 174: 評価入力の有無を判定。評価の入力欄に値が入っているかどうかを確認しています。

値がある場合だけ評価を更新する処理を実行します。LINE 175: 評価更新APIを呼び出し。入力された書籍IDを使って、評価更新用のAPIエンドポイントへリクエストを送信しています。

URLの一部に書籍IDを埋め込んでいます。LINE 176: HTTPメソッドをPATCHに指定。サーバーへ送るリクエストの種類をPATCHに設定しています。

評価の値だけを部分的に更新するために使います。LINE 177: 送信データの形式を指定。リクエストのヘッダーで、送信するデータがJSON形式であることをサーバーに伝えています。

LINE 178: 評価を数値に変換して送信。入力された評価の値を数値型に変換し、JSON文字列にしてリクエストの本文として設定しています。LINE 179: fetch呼び出しの終了。

評価更新のためのfetch処理の記述を閉じています。ここまでの設定内容でリクエストが送信されます。LINE 180: if文の終了。

評価が入力されている場合の処理をここで終了しています。LINE 181: 書籍一覧を再読み込み。更新処理が終わった後に、画面上の書籍一覧を最新の状態に更新するための関数を呼び出しています。

LINE 182: クリックイベント処理の終了。記録更新ボタンが押されたときの一連の処理をここで閉じています。LINE 184: 初回の書籍一覧を読み込み。

ページが読み込まれた直後に一覧表示用の関数を実行し、登録済みの書籍を画面に表示しています。LINE 185: scriptタグの終了。画面の動作を記述したJavaScriptの部分をここで終了しています。

LINE 186: bodyタグの終了。HTMLの本文部分をここで終了しています。LINE 187: HTML文字列の終了。

画面全体を表すHTML文字列の記述をここで閉じています。この文字列がINDEX_HTMLという変数に格納されています。LINE 190: トップページのルートを定義。

ブラウザがトップページにアクセスしたときに呼び出される処理を定義しています。レスポンスの形式をHTMLとして指定しています。LINE 191: index関数の定義開始。

トップページにアクセスがあったときに実行される関数を定義しています。LINE 192: HTML画面を返却。あらかじめ用意しておいた画面用のHTML文字列をブラウザへ返しています。

これにより読書記録アプリの画面が表示されます。RUN 9/9: トップページ画面の配信を確認する。ルートURLへアクセスし、読書記録アプリのHTMLが返ることを確認します。

CHECK 9/9: 途中実行に成功。True RETURN 09: エディターへ戻りました。入力済みのコードを維持したまま、次の実装へ進みます。

LINE 195: ルート一覧を定義。アプリで用意されている画面のパスを一覧として変数にまとめています。トップページのパスが含まれています。

PREVIEW: 完成した読書記録アプリを操作。入力・送信・結果表示を実際のブラウザ画面で確認します。UPLOAD: 実際の入力データでアプリを操作。

書名と著者を入力して書籍を登録し、続けて読了日と評価を更新すると、一覧に読了日と5段階評価の星が反映されることを確認できました。PREVIEW: 完成した読書記録アプリを操作。入力・送信・結果表示を実際のブラウザ画面で確認します。

UPLOAD: 実際の入力データでアプリを操作。書名と著者を入力して書籍を登録し、続けて読了日と評価を更新すると、一覧に読了日と5段階評価の星が反映されることを確認できました。確認1/5: 書籍を登録する。

書名と著者を送って新しい書籍を登録し、返ってきた内容を確認します。確認2/5: 登録済みの本を一覧で確認する。複数の書籍を登録し、id順の一覧が取得できることを確認します。

確認3/5: 読了日と評価を記録する。登録した本に読了日と評価を追記し、保存結果を確認します。確認4/5: 範囲外の評価を弾く。

1〜5の範囲を超えた評価を送ると400エラーが返ることを確認します。確認5/5: トップページの画面を表示する。ルートURLへアクセスし、読書記録アプリの画面が返ることを確認します。

学習内容のまとめ。table=Trueでテーブルとして扱うクラス指定POST /api/booksで書名と著者を登録 登録ボタンでPOSTを送り一覧を再取得SQLiteのインメモリデータベースでデータを保持する構成 小さく実行確認しながら完成状態まで段階的に組み立てる エンディング。Python研修はCodeCampでご確認ください。

SQLModelとFastAPIとは

今回使用する主要なライブラリについて、役割と使い分けを順番に確認します。

SQLModelでデータベースを扱う仕組み

SQLModelは、Pythonのクラス定義をそのままデータベースのテーブルとして扱えるORMライブラリです。SQLAlchemyとPydanticを組み合わせて作られており、型ヒントを書くだけで列の種類や初期値が決まります。今回はBookクラスをSQLModelで定義し、書名や著者、読了日、評価をひとつの表にまとめました。

データベースへの読み書きはSessionを通して行うため、SQL文を直接書かずに操作できる点が魅力になります。

Bookモデルを定義し一覧を取り出すときに指定した設定は次のとおりです

  • table=Trueでテーブルとして扱うクラス指定
  • Field(primary_key=True)で自動採番する主キー
  • Optional[date]で未入力を許す読了日の列
  • Optional[int]で後から設定する評価の列
  • selectとorder_byでid順に取り出す一覧取得

FastAPIでAPIと画面を配信する役割

FastAPIは、PythonでWeb APIを短いコードで構築できるWebフレームワークです。関数にデコレーターを付けるだけでURLと処理が結び付き、リクエストの検証やJSONへの変換は自動で行われます。今回は4つのAPIに加えて、トップページのHTMLもFastAPIから返す構成にしました。

リクエストの形はPydanticのBaseModelで定義し、想定外の入力を早い段階で弾けるようにしています。

今回FastAPIで公開したエンドポイントの構成は次のとおりです

  • POST /api/booksで書名と著者を登録
  • PATCH /finishedで読了日を記録
  • PATCH /ratingで1〜5の評価を記録
  • GET /api/booksで登録済みを一覧取得
  • GET /でHTML画面を配信
さらに詳しく学べるPython研修の詳細はこちら

Python・SQLModelで開発する場合の環境構築

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

python -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install "fastapi==0.115.6" "sqlmodel==0.0.22" "uvicorn[standard]==0.30.6" "pysqlite3-binary==0.5.3"

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

python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install "fastapi==0.115.6" "sqlmodel==0.0.22" "uvicorn[standard]==0.30.6" "pysqlite3-binary==0.5.3"
  • SQLite自体はPython同梱のsqlite3モジュールで動作しますが、環境によって組み込みのSQLiteが古い場合に備え、最新のSQLiteバイナリを含むpysqlite3-binaryも合わせてインストールしています。
  • 起動は .\.venv\Scripts\python.exe -m uvicorn reading_log_api:app --reloadで、ブラウザからhttp://127.0.0.1:8000/ を開くと画面が表示されます。
さらに詳しく学べるPython研修の詳細はこちら

PythonのSQLModelとFastAPIで作る読書記録アプリの要件定義

目的は、SQLModelとFastAPIとSQLiteで書名・著者・読了日・評価を保存し、ブラウザから登録と一覧表示まで確認できる読書記録アプリを作ることです。

対象者として、Pythonの基本文法を学び終え、SQLModelとFastAPIでデータベース付きのWeb APIを一から作る手順を学びたい人を想定しています。

完成物は、SQLModelでモデルを定義し、FastAPIの4つのAPIとブラウザ画面から本の登録・読了日・評価を記録できる1ファイルの読書記録アプリです。

実装へ入る前に、機能・品質・受け入れ条件を分けて確認します。

機能要件

  • 書名と著者を送って書籍を登録する機能
  • 登録済みの書籍をid順で一覧取得する機能
  • 書籍IDを指定して読了日を記録する機能
  • 書籍IDを指定して1〜5の評価を記録する機能
  • 評価を★の数で表示し未評価を区別する画面
  • トップページで操作用のHTML画面を配信する機能

非機能要件

  • SQLiteのインメモリデータベースでデータを保持する構成
  • StaticPoolで単一接続を共有する設定
  • check_same_threadをFalseにする接続設定
  • 評価を1〜5の範囲に制限するバリデーション
  • 存在しない書籍IDに404を返す例外処理
  • モデル定義からHTML配信までを1ファイルにまとめる構成

実装方針

今回はSQLModelとFastAPIの基本動作を追いやすくするため、読書記録アプリ本体を1つのPythonファイルへまとめます。

入力、判定、結果表示の役割を分け、実行結果を確認しながら機能を積み上げます。

読書記録アプリを安全に組み立てるための実装方針は次のとおりです。

  • SQLiteのインメモリデータベースでデータを保持する構成
  • StaticPoolで単一接続を共有する設定
  • check_same_threadをFalseにする接続設定
  • 評価を1〜5の範囲に制限するバリデーション
  • 存在しない書籍IDに404を返す例外処理
  • モデル定義からHTML配信までを1ファイルにまとめる構成

完成と判断する条件

  1. POSTした書名と著者が登録済み書籍として返る
  2. GET /api/booksで登録した本がid順に返る
  3. PATCH /finishedで読了日が保存される
  4. PATCH /ratingで1〜5の評価が保存される
  5. 範囲外の評価に400エラーが返る
  6. 存在しないIDに404エラーが返る
  7. ルートURLで読書記録アプリのHTMLが返る
さらに詳しく学べるPython研修の詳細はこちら

SQLModelとFastAPIで読書記録アプリを作る際の重要ポイント

このアプリは、書籍の登録から読了日・評価の記録までを、ひとつの画面とデータベースで完結できる作りです。ブラウザ側のJavaScriptがfetchでAPIを呼び出し、結果を一覧へ描き直す仕組みになっています。登録直後の本は読了日も評価も空のままで、読み終えたタイミングで追記可能です。

評価は星マークへ変換して表示されるため、一覧を見ただけで読後の満足度が伝わります。

ブラウザとAPIがやり取りする際の処理の流れは次のとおりです

  • 登録ボタンでPOSTを送り一覧を再取得
  • 更新ボタンで読了日と評価をまとめて送信
  • 空欄の項目は送らず既存の値を保持
  • ratingを★の繰り返しに変換して表示
  • 未入力の読了日は未読了と表示

本を登録して一覧へ反映する流れ

登録ボタンを押すと、書名と著者がPOSTで送られ、新しい書籍がデータベースへ保存されます。保存が終わると入力欄が空になり、loadBooksが呼ばれて最新の一覧に更新されます。

登録ボタンを押したときに起きる処理の順番は次のとおりです

  • 入力値をtrimして空なら送信を中止
  • /api/booksへJSONでPOST
  • 入力欄をクリア
  • loadBooksで一覧を再描画

読了日と評価を後から更新する流れ

管理セクションでは書籍IDを指定し、読了日と評価を必要な分だけ更新できます。読了日と評価は別々のAPIに分かれており、入力があった項目だけがPATCHで送信されます。

更新ボタンが送信するリクエストの条件は次のとおりです

  • 読了日が入力された時だけ/finishedへPATCH
  • 評価が入力された時だけ/ratingへPATCH
  • IDが空なら何も送らず中止

評価と読了状態を見やすく表示する処理

一覧の各行は、書籍IDと書名、著者に続けて読了日と評価を並べます。評価はratingの数だけ★を繰り返し、未評価や未読了のときは文字で状態を示します。

一覧の1行に表示される情報の並びは次のとおりです

  • 先頭に#付きの書籍ID
  • 書名と著者をスラッシュ区切りで表示
  • 読了日または未読了の文字
  • ★の数または未評価の文字

Python研修一覧はこちら

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

Python研修を比較する

Java研修一覧はこちら

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

Java研修を比較する

PHP研修一覧はこちら

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

PHP研修を比較する

新入社員研修

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

新入社員研修を比較する

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

Pythonで読書記録アプリの完成コード

コードは大きく4つの部品でできています。SQLModelのモデル、リクエスト用のスキーマ、SQLiteのエンジン、そして4つのAPIです。

モデルはテーブルの形を表し、スキーマは受け取るJSONの形を表します。役割を分けることで、保存する列と入力させる項目を別々に管理できる仕組みです。

各APIはSessionを使ってデータベースへ読み書きします。更新系は対象の書籍をsession.getで取り出し、値を書き換えてから保存する流れになります。

このセクションの用語

モデル
データベースの表をPythonのクラスとして表したものです。列がクラスの属性に対応します。
スキーマ
APIが受け取るJSONの形を決める型定義です。ここではBaseModelで書いています。
create_engine
データベースへの接続をまとめて管理する入り口を作る関数です。
Session
データベースとの一連のやり取りをまとめる作業単位です。追加や保存をこの中で行います。
primary_key
各行を一意に識別するための列です。ここではidが該当します。
select
保存済みのデータを問い合わせて取り出すための命令です。
APIがリクエストを保存して返すまで fetchがJSONをAPIへ送信スキーマがJSONを検証SessionがSQLiteへ保存refreshでidを取得保存結果をJSONで返す
APIがリクエストを保存して返すまで
from datetime import date
from typing import Optional

from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from sqlalchemy.pool import StaticPool
from sqlmodel import Field, Session, SQLModel, create_engine, select


# データモデルの定義(SQLModelでテーブルを表現)
class Book(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    author: str
    finished_on: Optional[date] = Field(default=None)
    rating: Optional[int] = Field(default=None)


# リクエストボディのスキーマ
class BookCreate(BaseModel):
    title: str
    author: str


class FinishedUpdate(BaseModel):
    finished_on: date


class RatingUpdate(BaseModel):
    rating: int


# データベース(SQLite)の準備
engine = create_engine(
    "sqlite://",
    connect_args={"check_same_thread": False},
    poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)


# FastAPIアプリの生成
app = FastAPI(title="読書記録アプリ")


# 書籍を登録するAPI
@app.post("/api/books")
def create_book(payload: BookCreate):
    book = Book(title=payload.title, author=payload.author)
    with Session(engine) as session:
        session.add(book)
        session.commit()
        session.refresh(book)
        return book


# 読了日を管理するAPI
@app.patch("/api/books/{book_id}/finished")
def update_finished(book_id: int, payload: FinishedUpdate):
    with Session(engine) as session:
        book = session.get(Book, book_id)
        if book is None:
            raise HTTPException(status_code=404, detail="書籍が見つかりません")
        book.finished_on = payload.finished_on
        session.add(book)
        session.commit()
        session.refresh(book)
        return book


# 評価を管理するAPI
@app.patch("/api/books/{book_id}/rating")
def update_rating(book_id: int, payload: RatingUpdate):
    if not 1 <= payload.rating <= 5:
        raise HTTPException(status_code=400, detail="評価は1〜5で指定してください")
    with Session(engine) as session:
        book = session.get(Book, book_id)
        if book is None:
            raise HTTPException(status_code=404, detail="書籍が見つかりません")
        book.rating = payload.rating
        session.add(book)
        session.commit()
        session.refresh(book)
        return book


# 登録済みの書籍を一覧取得するAPI
@app.get("/api/books")
def list_books():
    with Session(engine) as session:
        return session.exec(select(Book).order_by(Book.id)).all()


# 画面(HTML/JS)の配信
INDEX_HTML = """<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>読書記録アプリ</title>
<style>
  body { font-family: system-ui, sans-serif; margin: 24px; max-width: 640px; }
  section { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
  input { padding: 6px; margin: 4px 4px 4px 0; }
  button { padding: 6px 14px; cursor: pointer; }
  li { margin: 6px 0; }
</style>
</head>
<body>
<h1>読書記録アプリ</h1>

<section id="register">
  <h2>書籍を登録</h2>
  <input id="book-title" placeholder="書名">
  <input id="book-author" placeholder="著者">
  <button id="add-book">登録する</button>
</section>

<section id="manage">
  <h2>読了日と評価を管理</h2>
  <input id="target-id" value="1" placeholder="書籍ID">
  <input id="finished-input" placeholder="読了日 (YYYY-MM-DD)">
  <input id="rating-input" type="number" min="1" max="5" placeholder="評価 1〜5">
  <button id="save-progress">記録を更新する</button>
</section>

<section id="library">
  <h2>登録済みの本</h2>
  <ul id="book-list"></ul>
</section>

<script>
async function loadBooks() {
  const res = await fetch('/api/books');
  const books = await res.json();
  const list = document.getElementById('book-list');
  list.innerHTML = '';
  for (const b of books) {
    const li = document.createElement('li');
    const stars = b.rating ? '★'.repeat(b.rating) : '未評価';
    const finished = b.finished_on ? b.finished_on : '未読了';
    li.textContent = '#' + b.id + ' ' + b.title + ' / ' + b.author
      + ' — 読了日: ' + finished + ' 評価: ' + stars;
    list.appendChild(li);
  }
}

document.getElementById('add-book').addEventListener('click', async () => {
  const title = document.getElementById('book-title').value.trim();
  const author = document.getElementById('book-author').value.trim();
  if (!title || !author) return;
  await fetch('/api/books', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title, author })
  });
  document.getElementById('book-title').value = '';
  document.getElementById('book-author').value = '';
  await loadBooks();
});

document.getElementById('save-progress').addEventListener('click', async () => {
  const id = document.getElementById('target-id').value.trim();
  if (!id) return;
  const finished = document.getElementById('finished-input').value.trim();
  const rating = document.getElementById('rating-input').value.trim();
  if (finished) {
    await fetch('/api/books/' + id + '/finished', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ finished_on: finished })
    });
  }
  if (rating) {
    await fetch('/api/books/' + id + '/rating', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ rating: Number(rating) })
    });
  }
  await loadBooks();
});

loadBooks();
</script>
</body>
</html>"""


@app.get("/", response_class=HTMLResponse)
def index():
    return INDEX_HTML


ROUTES = ["/"]

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

Bookモデルでテーブルを定義する

class Book(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    author: str
    finished_on: Optional[date] = Field(default=None)
    rating: Optional[int] = Field(default=None)

table=Trueを付けたクラスがそのままテーブルになります。idは自動採番の主キーで、読了日と評価はあとから入れるのでOptionalにして初期値を空にしています。

BookCreateで入力項目だけを受け取る

class BookCreate(BaseModel):
    title: str
    author: str

登録時に必要なのは書名と著者だけです。保存用のBookとは別にスキーマを用意すると、idや評価をユーザーに入力させずに済みます。

create_engineでSQLiteを準備する

engine = create_engine(
    "sqlite://",
    connect_args={"check_same_thread": False},
    poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)

sqlite://はメモリ上のデータベースを指します。create_allを呼ぶとモデルからテーブルが作られ、以降のAPIが読み書きできる状態になります。

create_bookで書籍を登録する

@app.post("/api/books")
def create_book(payload: BookCreate):
    book = Book(title=payload.title, author=payload.author)
    with Session(engine) as session:
        session.add(book)
        session.commit()
        session.refresh(book)
        return book

受け取った書名と著者からBookを作り、session.addsession.commitで保存します。session.refreshで採番されたidを読み直し、その本を返します。

update_finishedで読了日を書き込む

@app.patch("/api/books/{book_id}/finished")
def update_finished(book_id: int, payload: FinishedUpdate):
    with Session(engine) as session:
        book = session.get(Book, book_id)
        if book is None:
            raise HTTPException(status_code=404, detail="書籍が見つかりません")

URLのbook_idで対象を探し、見つからなければ404を返します。存在チェックを入れておくと、無い本を更新しようとしたときに原因が分かりやすくなります。

update_ratingで評価の範囲を検証する

    if not 1 <= payload.rating <= 5:
        raise HTTPException(status_code=400, detail="評価は1〜5で指定してください")

評価は1から5までの想定なので、範囲外なら400を返して弾きます。保存前にチェックすると、星の数がおかしくなる登録を防げます。

list_booksで一覧を取り出す

@app.get("/api/books")
def list_books():
    with Session(engine) as session:
        return session.exec(select(Book).order_by(Book.id)).all()

select(Book)で全件を取り出し、order_by(Book.id)で登録順に並べます。all()でリストにして返すと、画面側がそのまま並べて表示できます。


Python基礎・実践(Django)

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

Python研修の詳細

DX社員研修

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

DX研修の詳細

Javaエンジニア育成研修

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

Java研修の詳細

新卒・新入社員向け研修

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

新入社員研修の詳細

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

読書記録アプリの動作確認

実際にブラウザでルートの画面を開き、フォームから操作してみました。まず書名と著者を入力し、登録ボタンで1冊を追加します。

続けて読了日と評価を更新すると、一覧に読了日と5段階評価の星が反映されることを確認できました。登録直後の評価は空欄で、更新後に星が現れる変化も画面で追えます。

取得キャプチャには操作前の画面と、更新後に一覧が変わった画面が残っています。どちらもHTTP 200が返り、エラーは出ていません。

このセクションの用語

fetch
ブラウザのJavaScriptからサーバのAPIを呼び出す関数です。今回は各APIの送受信に使っています。
HTTP 200
リクエストが正しく処理されたことを表すステータスコードです。
操作前の画面: / (HTTP 200)
操作前の画面: / (HTTP 200)
書名と著者を入力して書籍を登録し、続けて読了日と評価を更新すると、一覧に読了日と5段階評価の星が反映されることを確認できました。
書名と著者を入力して書籍を登録し、続けて読了日と評価を更新すると、一覧に読了日と5段階評価の星が反映されることを確認できました。
実ブラウザ操作: 書名と著者を入力して書籍を登録し、続けて読了日と評価を更新すると、一覧に読了日と5段階評価の星が反映されることを確認できました。
実ブラウザ操作: 書名と著者を入力して書籍を登録し、続けて読了日と評価を更新すると、一覧に読了日と5段階評価の星が反映されることを確認できました。
さらに詳しく学べるPython研修の詳細はこちら

PythonのSQLModelとFastAPIのエラー対処

ここでは実装中に出会いやすいエラーを整理します。多くはテーブル未作成やリクエストの形の食い違いが原因です。

エラーメッセージには手がかりが含まれます。ステータスコードとdetailの文言を読むと、原因の切り分けが速くなります。

このセクションの用語

422エラー
送ったJSONが型やキーの条件を満たさないときにFastAPIが返すエラーです。
ステータスコード
リクエストの結果を数値で表す仕組みです。200は成功、4xxは要求側の誤りを示します。
エラー例 原因 対処
no such table: book create_allより前にAPIを呼び出した 起動時にSQLModel.metadata.create_all(engine)を実行する
422 Unprocessable Entity titleやauthorが欠けたJSONを送った スキーマ通りのキーと型でリクエストを送る
404書籍が見つかりません 存在しないbook_idを指定した 先にGET /api/booksでidを確認する
400評価は1〜5で指定してください ratingに範囲外の数値を送った 1から5の整数を送る
ModuleNotFoundError: sqlmodel SQLModelが未インストール pip install sqlmodelで導入する

読書記録アプリで注意したい点

つまずきやすいのは、目に見えないデータベース側の準備です。テーブルを作る前にAPIを呼ぶと、存在しない表への操作で止まります。

評価の範囲チェックも見落としがちです。今回はupdate_ratingで1〜5に限定し、範囲外を400で弾くようにしています。

保存用のBookと入力用のBookCreateを分ける設計も、最初は迷いやすい部分です。ポイントとしては、テーブル作成・評価の範囲・スキーマ分離の3点を先に押さえておくと安心です。

POINT

テーブル作成:create_allを先に実行

評価の範囲:ratingは1〜5に制限

スキーマ分離:登録用と保存用を分ける

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

読書記録APIをPythonで活用できる場面

この構成は、記録して一覧で振り返る形のアプリに広く応用できます。書名・日付・評価という3つの列を、扱いたい対象に置き換えるだけで転用しやすいです。

小さく作って動かせるので、個人の記録から社内の共有ツールの試作まで幅広く役立ちます。

使える場面 具体的な使い方
個人の読書管理 読んだ本と評価を残し、月ごとの読了数を振り返る
技術書の学習ログ 進捗と5段階評価を記録し、再読の優先度を決める
社内の書籍共有 部署の蔵書に評価を付け、おすすめ本の判断材料にする
記録アプリの試作 日付と評価の列を差し替え、映画や勉強の記録に転用する
さらに詳しく学べるPython研修の詳細はこちら

読書記録アプリ開発のまとめ

SQLModelとFastAPIを1ファイルにまとめ、SQLiteに読書記録を保存するAPIを作りました。書名と著者の登録から、読了日と評価の更新まで一通り動きます。

実際に操作すると、一覧へ読了日と星が反映されることを確認できました。モデル・スキーマ・エンジン・APIという部品の役割が見えると、他の記録アプリにも応用しやすくなります。

まずは手元で登録と更新を試し、列や検証ルールを自分の用途に合わせて広げてみてください。

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

参考にした一次情報

  1. ^ SQLModel公式ドキュメント. https://sqlmodel.tiangolo.com/, (参照26-07-31).
  2. ^ FastAPI公式ドキュメント. https://fastapi.tiangolo.com/, (参照26-07-31).
  3. ^ Python標準ライブラリsqlite3. https://docs.python.org/3/library/sqlite3.html, (参照26-07-31).
  4. ^ SQLite公式サイト. https://www.sqlite.org/index.html, (参照26-07-31).

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

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やプログラムなどの
最新情報を検索する