このハンズオンはPythonのWebフレームワークであるFastAPIを使ったWebアプリケーションを開発するハンズオンの資料です。

ハンズオンの構成

ハンズオンは以下の構成で進めていきます。

Step1では簡単なAPIを使ってWebアプリケーションの動かす仕組みを理解していきます

Step2では使用したAPIの使い方をわかりやすく整理するためにOpenAPIのドキュメントを作成します。

Step3ではデータベースを使ったAPIを開発していきます。

Step4ではHTMLを用意してStep1〜4で作成したAPIを使用したWebアプリケーションを開発していきます。

最終的には以下のスクショにある簡易的な掲示板アプリを作成することを目指します。

FastAPIについて解説

FastAPIはPythonのWebフレームワークの中では比較的新しいフレームワークで、非同期処理に対応しています。Flaskに近い記法なのでFlask触ったことある方なら同じ感覚でAPIを実装できます。

名前の通り、公式ドキュメント上ではNode.jsやGo言語で作成したWebアプリケーションと同じぐらい高速な処理を実現していると述べています。また、処理だけではなく型定義などを明確にすることでより高速に開発することをサポートしてくれます。

APIの動作確認にはOpenAPIというドキュメントを使用することになりますが、これはFastAPIのデフォルトの機能として提供されており、リクエストとレスポンスのスキーマをAPIごとに定義するだけで効率よくAPIドキュメントを作ることができます。これらも前述した型定義を明確にすることでAPIの定義が明確になりAPIの動作確認も簡単になります。

Djangoよりも機能が簡易的なため、動作が軽い一方でデータベースと接続したり、HTMLで画面表示をする時などには他のライブラリ、プラグインを追加する必要があります。そのあたりは本ハンズオンでも言及していきます。

次のページから早速ハンズオンを進めていきましょう。

Gitpodのワークスペースを用意する

今回は簡単に環境構築をできるようにGitpodというサービスを使用してオンラインの開発環境を構築します。以下のリポジトリにアクセスします。

https://github.com/Miura55/gitpod-python-playground

レポジトリの下部にあるREADMEにある Open in Gitpod をクリックすることで今回のハンズオンの開発を構築します。

以下のワークスペース作成画面が表示されるので、IDEが「VSCode -Browser-」になっていることを確認します。手元にVS Code、またはJetBrain系のIDEをお持ちの場合はDesktop版を選択しても結構です。セットアップについては以下のリンクのドキュメントを参考に必要な拡張機能のセットアップを行ってください。

https://www.gitpod.io/docs/references/ides-and-editors/vscode

以下のようにオンラインのIDEが起動されたら開発環境構築は完了です。

ここまでできたらハンズオンの準備は完了です。次のステップでからハンズオンを進めていきましょう。

サーバーを立ち上げる

最初のステップとして、シンプルなAPIを開発していきます。

まずは今回開発するアプリケーションのソースコードを用意します。以下のコマンドを実行して新規でファイルを作成します。

touch /workspace/gitpod-python-playground/main.py

ターミナルに貼り付けるときに以下のダイアログが表示されたらコピペを許可することでエディタ上でコピペができるようになります。

テキストエディタで上記のコマンドで作成した main.py を開き、以下のコードを書き込みます。

from fastapi import FastAPI, Depends, HTTPException, Request
import logging

app = FastAPI()
logger = logging.getLogger('uvicorn')

@app.get("/")
async def root():
    return {"message": "Hello World"}

Gitpodはソースコードが自動保存されるので、このままターミナルを起動します。ターミナルは Ctrl + ` (Macの場合はCtrl + `) を入力することで起動します。

起動したら以下のコマンドを実行してアプリケーションを立ち上げます。

uvicorn main:app --reload --host 0.0.0.0

実行後、ターミナルで以下のようなログが出力されれば、サーバーは正常に起動しています。

これでコードが修正されて保存される度にサーバーが自動で立ち上げ直してくれるようになります。

INFO:     Will watch for changes in these directories: ['/workspace/gitpod-python-playground']
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [17711] using StatReload
INFO:     Started server process [17713]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

起動するとエディタの右下にある Make Public をクリックすることで立ち上げたサーバーを公開できる状態になります。

エディタの下にある PORTS タブをクリックすると払い出されたポートのURLが一覧で表示されます。この一覧の中にある8000番ポートの行にある地球のアイコンをクリックするとブラウザで起動しているWebページを開くことができます。

別タブが開き、以下のJSONが表示されたらサーバーの接続は問題ありません。開いた時に使用したURLはこの後に手順で必要になるのでどこかにメモしておきます。

データをサーバーに送信する

次はこのサーバーに何かしらのデータを送信する機能を実装します。今回は、掲示板アプリに投稿するのに必要なメッセージを送信できるようにしてみます。

送信する内容は以下の形式のJSONです。

{
  "title": "初投稿"
  "content": "Hello World"
  "user_name": "Taro"
}

まずはこのJSONを送信するためにスキーマを定義します。すでにサーバーを立ち上げているので、ここからのコマンド操作はサーバーを起動しているターミナルとは別にもう一つターミナルを開きます。ターミナルはターミナルの右上の+ボタンをクリックすると開くことができます。

以下のコマンドを実行してJSONのフォーマットを定義するためのファイルを新規で作成します。

touch /workspace/gitpod-python-playground/schemes.py

schemes.py のファイルを開き、以下のコードを書き込みます。

from pydantic import BaseModel

class EntryRequest(BaseModel):
    title: str
    content: str
    user_name: str

保存したら、 main.py を開いて以下の1行を import文 の記述に追加します。

import schemes

import文の記述が以下の通りになっていれば問題ありません。

from fastapi import FastAPI
import logging
import schemes

続けて main.py の最後に以下の関数を追加します。

@app.post("/entry")
async def create_entry(entry: schemes.EntryRequest):
    logger.info(f"Received entry: {entry}")
    return {"message": "Entry created"}

記述が終わるとコードが自動で保存されてサーバーが再起動します。再起動したら、サーバーを起動しているターミナルとは別にもう一つターミナルを開きます。ターミナルはターミナルの右上の+ボタンをクリックすると開くことができます。

以下の2行のコマンドでサーバーにデータを送信してみます。【メモしたGitpodのサーバーURL】の箇所は先ほどブラウザの表示を確認した時にメモしたGitpodのサーバーURLに書き換えます。 export SERVER_URL=【メモしたGitpodのサーバーURL】 のときにURLの末尾に / を含めないように気をつけます。

export SERVER_URL=【メモしたGitpodのサーバーURL】
curl -X POST ${SERVER_URL}/entry -H "Content-Type: application/json" -d '{"title": "初投稿", "content": "Hello World", "user_name": "Taro"}'

コマンドを実行し、サーバーのレスポンスである以下の実行結果が表示されていればサーバーは正常に動作しています。もしエラーが出る場合は変数のURLを設定した時に不要なスペースなどが無いかご確認ください。

コマンドを実行したターミナルはこの後実装するAPIの動作確認でも使用するので閉じなくて結構です。

{"message":"Entry created"}

アプリケーションを立ち上げているサーバーを動かしているターミナルを確認すると以下の通りログが出力されてサーバーでコマンドで送信したデータが受け取れていることが確認できます。

INFO:     Received entry: title='初投稿' content='Hello World' user_name='Taro'

パスパラメータ

続いて、パスパラメータを扱うAPIを実装します。パスパラメータとは簡単に言うとURLの中に埋め込まれた値をアプリケーションの変数として扱うことを指します。例えば、ユーザーIDを指定してそのユーザーの情報を取得するときなどに活用されます。今回は掲示板の投稿IDを指定することでその投稿が削除されるAPIを用意します。

main.py の一番下に以下の関数を追加します。

@app.delete("/entry/{entry_id}")
async def delete_entry(entry_id: int):
    logger.info(f"Entry {entry_id} deleted")
    return {"message": "Entry deleted"}

書き込みを終えて、自動保存と再起動が行われたら、POSTのAPIを実行した時に使用したターミナルを開き、以下のコマンドを実行します。

export SERVER_URL=【メモしたGitpodのサーバーURL】
curl -X DELETE ${SERVER_URL}/entry/1

コマンドを実行し、以下のJSONで実行結果が表示されたらAPIは正常に動作しています。

{"message":"Entry 1 deleted"}

ちなみにこのAPIはint型しか受け付けないので、それ以外の型でパスパラメータのリクエストを送信すると弾かれるように作られています。試しに以下のリクエストで文字列を送信してみます。

curl -X DELETE ${SERVER_URL}/entry/hello

実行後、以下の通りエラーがレスポンスとして返ってきます。

{
  "detail": [
    {
      "type": "int_parsing",
      "loc": [
        "path",
        "entry_id"
      ],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "hello"
    }
  ]
}

ここまでFastAPIで最小構成でAPIを用意しました。APIの通信を実装、コマンドを通じて理解していただけたかと思います。

ここではAPIの動作確認をCurlコマンドを使用していましたが、実はFastAPIの標準機能にあるOpenAPIを使うことでブラウザ上でAPIの動作確認をすることができます。そこで次のパートではOpenAPIを使ってAPIを実行する方法を紹介します。

Step1ではAPIを実装し、そのAPIの動作確認をCurlコマンドで行いました。通常のフレームワークでAPIを動作確認する時にはこの方法で行うのが一番簡単ですが、FastAPIであればもっと簡単にAPIの動作確認をする方法として、OpenAPIを使う方法があります。

OpenAPIとはWebアプリケーションとAPIで通信を行うための規格を指しています。FastAPIはそのOpenAPIに準拠したAPIを構築できるような設計になっており、APIを実装するだけでOpenAPIの規格に倣ったAPIの仕様書が生成されるようになります。この仕様書とその動作確認をするためのUIが提供されています。

まずは動かしてみる

まずは、OpenAPIのドキュメントを開いてみます。OpenAPIのドキュメントはブラウザで 【GitpodのサーバーURL】/docs で開くと以下のAPIのドキュメントが表示されます。

試しに「GET」の行にあるトグルをクリックするとAPIの情報が表示されます。

試しに「Try it out」ボタンをクリックした時に表示される「Execute」ボタンをクリックすると以下の通り実行したAPIのCurlコマンド、及びその実行結果が表示されます。

ここで表示されているCurlコマンドをそのままターミナルで実行すれば、同様の結果が返ってくることが確認できます。なので、作者もFastAPIでAPIを実装したらまずはこのOpenAPIを使って動作確認を行い、その後ターミナルで繰り返し動作確認したいときには、OpenAPIが生成したCurlコマンドを実行するということも多いです。

OpenAPIで仕様を詳細に定義する

レスポンスのスキーマを定義する

OpenAPIを使ってAPIドキュメントを生成することができますが、現状の実装で生成されたAPIドキュメントは仕様書としては不完全なものです。なぜなら、APIのリクエストは詳細に定義されていますが、そのレスポンスのスキーマは明確には定義されていないからです。

先ほど動作確認した GET / のAPIのレスポンスを確認するとレスポンスとしてJSONが表示されるはずですが、ドキュメント上ではデフォルトの設定である文字列を返すようになっています。

FastAPIではリクエストと同様にレスポンスのスキーマもコードで定義すれば、APIドキュメントにその内容を反映させることができます。

Step1で実装したAPIのレスポンスはすべて共通のJSONになっているので、そのスキーマを定義します。

shemes.py を開き、以下のClass文を追加します。

class MessageResponse(BaseModel):
    message: str

追加後にAPIの定義をしている行に以下の通り response_model=schemes.MessageResponse を追加します。

@app.get("/", response_model=schemes.MessageResponse)
...

@app.post("/entry", response_model=schemes.MessageResponse)
...

@app.delete("/entry/{entry_id}", response_model=schemes.MessageResponse)
...

自動保存され再起動されたら、ドキュメントをリロードします。リロードすると以下の通りAPIのレスポンスのフォーマットが表示されることが確認できます。

サンプルのパラメータを設定する

リクエスト、レスポンスボディをそれぞれ定義しました。ただ、ここでは文字列を送信するということでサンプルのパラメータはすべて string になっていますが、もう少し具体的なパラメータをイメージできるようにサンプルのパラメータを設定することもできます。

そのやり方は簡単で schemes.py で定義した型にそれぞれ適切なサンプルパラメータを設定するだけです。

今回用意した schemes.py の場合では以下の通りサンプルのパラメータを定義してみます。

class EntryRequest(BaseModel):
    title: str = 'Title'
    content: str = 'This is content'
    user_name: str = 'User'


class MessageResponse(BaseModel):
    message: str = 'This message is example'

自動保存、サーバーが再起動されたら再びドキュメントをリロードしてみます。すると、以下の通りスキーマで設定したパラメータがドキュメントに反映されていることが確認できます。

APIの説明を加える

これでAPIのリクエストとレスポンスがドキュメントがイメージできるようになりました。ただ、APIのリクエストとレスポンスだけがあってもそのAPIの使い方、どのような使い方をすべきかを簡単な説明があるとよりわかりやすくなります。

各APIに関する説明は、main.py にあるAPIのルーティング設定に引数 description で設定することでAPIの情報を反映させることができます。

@app.get("/", response_model=schemes.MessageResponse, description="接続確認用のAPIです。")
...

@app.post("/entry", response_model=schemes.MessageResponse, description="掲示板にエントリーを新規追加します。")
...

@app.delete("/entry/{entry_id}", response_model=schemes.MessageResponse, description="IDを指定して掲示板のエントリーを削除します。")
...

自動保存、サーバーの再起動が行われたらドキュメントをリロードしてみます。リロードして以下の通り description で設定したAPIの説明が表示されることが確認できます。

更にカスタマイズしてみる

最後にAPIドキュメントのタイトルを修正したり、ドキュメントに概要を追加してみます。

APIドキュメント全体の定義はFastAPIのインスタンスを定義する時にカスタマイズすることができます。つまり、 main.pyapp = FastAPI() を以下の通り修正することでAPIドキュメントを更新することができます。

app = FastAPI(
    title="FastAPI Tutorial",
    summary="このAPIはFastAPIのチュートリアルとして掲示板のAPIを提供します。",
)

自動保存、サーバーが再起動した後にドキュメントをリロードします。以下の通りドキュメントのタイトル、概要が更新されます。

これ以外にもドキュメントをカスタマイズすることができますので、詳細は以下のリンクをご確認いただけると参考になります。

https://fastapi.tiangolo.com/ja/tutorial/metadata/

Redoc

これまではSwagger形式のAPIドキュメントを紹介しましたが、Redocという形式でAPIドキュメントを表示させることができます。ブラウザで 【GitpodのサーバーURL】/redoc を開くと、以下のようなAPIドキュメントが表示されます。

パブリックに公開されているようなAPIドキュメントもFastAPIなら簡単に生成することができます。

ここまでのソースコード

ここまでで実装してきたソースコードを最後のまとめとして掲載します。エラーが出たときなどに参考用としてご確認ください。

main.py

from fastapi import FastAPI, Depends, HTTPException, Request
import logging
import schemes

app = FastAPI(
    title="FastAPI Tutorial",
    summary="このAPIはFastAPIのチュートリアルとして掲示板のAPIを提供します。",
)
logger = logging.getLogger('uvicorn')


@app.get("/", response_model=schemes.MessageResponse, description="接続確認用のAPIです。")
async def root():
    return {"message": "Hello World"}

@app.post("/entry", response_model=schemes.MessageResponse, description="掲示板にエントリーを新規追加します。")
async def create_entry(entry: schemes.EntryRequest):
    logger.info(f"Received entry: {entry}")
    return {"message": "Entry created"}

@app.delete("/entry/{entry_id}", response_model=schemes.MessageResponse, description="IDを指定して掲示板のエントリーを削除します。")
async def delete_entry(entry_id: int):
    logger.info(f"Entry {entry_id} deleted")
    return {"message": f"Entry {entry_id} deleted"}

schemes.py

from pydantic import BaseModel

class EntryRequest(BaseModel):
    title: str = 'Title'
    content: str = 'This is content'
    user_name: str = 'User'


class MessageResponse(BaseModel):
    message: str = 'This message is example'

まとめ

ここまでOpenAPIのAPIドキュメントの中身をカスタマイズする方法を紹介しました。通常だとサードパーティーのドキュメントジェネレーターを使って生成したりすることがありますが、FastAPIだとデフォルトの機能を駆使することでOpenAPIのドキュメントが簡単に生成できます。

スキーマの書き方がわかったところで、次のパートではデータベースをFastAPIで接続する方法を紹介します。

Webアプリケーションの開発において、データベースの利用は欠かせません。今回開発するアプリケーションでも掲示板で投稿したデータをデータベースに溜め込むようにする必要があります。

そこでこのパートではFastAPIでデータベースを接続する方法を紹介します。

今回のテーブル設計

データベースとの接続をする前に今回使用するデータベースのテーブル設計を紹介します。今回は掲示板で投稿されたデータを残すので以下のテーブル構成を用意していきます。

カラム名

キー

備考

id

int

primary

投稿ID

title

str

-

タイトル

content

str

-

本文

user_name

str

-

ユーザー名

created_at

datetime

-

投稿日時

モデルの作成

FastAPIを使ったデータベース操作について

テーブル設計を確認したところで今回のテーブル構成を構築するためのモデルを作成します。FastAPIではライブラリとしてDBのORM(DB操作をオブジェクト志向的に行うためのツール)を提供しているわけではないため、自分で用意する必要があります。

ここでは、FastAPIの公式ドキュメントでも紹介されているSQL Alchemyを使ったDBのモデル作成を行います。SQL Alchemyでは以下のSQLサーバーの接続に対応しています。

今回はローカルで手軽にDBを扱うので、SQLiteを使ってDBの接続を行いますが、DBの接続先、及び接続に必要になるドライバーをインストールすればSQLite以外のデータベースにも今回作成するモデルをそのまま使ってデータ操作ができます。

database.pyの実装

DBの操作方法について解説したところで、実際にモデルを作成していきます。

まずはデータベースの接続情報を定義するコードを用意します。

以下のコマンドで database.py を新規作成します。

touch /workspace/gitpod-python-playground/database.py

エディタで database.py を開き、以下のコードを記述します。

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

engine = create_engine("sqlite:///./database.sqlite")
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

models.pyの実装

続いて、今回使用するテーブルのモデルを実装します。

以下のコマンドで models.py を用意します。

touch /workspace/gitpod-python-playground/models.py

エディタで models.py を開き、以下のコードを記述します。

from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from database import Base


class Entry(Base):
    __tablename__ = "entries"

    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    title = Column(String)
    content = Column(String)
    user_name = Column(String)
    created_at = Column(DateTime, server_default=func.now())

スキーマを定義

DBの操作であるCRUD(Create, Read, Update, Delete)をより効率よく行うためにリクエスト、レスポンス同様スキーマを定義します。スキーマを定義することでORM上の型とプログラム上の型を明確にできます。

schemes.py を開き、以下のClass文を追加します。

class Entry(BaseModel):
    id: int
    title: str
    content: str
    user_name: str
    created_at: str

    class Config:
        orm_mode = True

CRUDの実装

スキーマを定義したらDBの操作を行うためのCRUDのコードを実装します。

以下のコマンドで crud.py を新規作成します。

touch /workspace/gitpod-python-playground/crud.py

curd.py のコマンドを開いたら以下のコードを記入します。

from sqlalchemy.orm import Session
import models
import schemes


def get_entries(db: Session):
    return db.query(models.Entry).all()

def create_entry(db: Session, entry: schemes.EntryRequest):
    db_entry = models.Entry(
        title=entry.title,
        content=entry.content,
        user_name=entry.user_name
    )
    db.add(db_entry)
    db.commit()
    db.refresh(db_entry)
    return db_entry

def delete_entry(db: Session, entry_id: int):
    db_entry = db.query(models.Entry).filter(models.Entry.id == entry_id).first()
    if db_entry is None:
        raise AttributeError(f"Entry {entry_id} not found")
    db.delete(db_entry)
    db.commit()
    return db_entry

アプリケーションと接続

これで一通りデータベースを操作するための準備ができたので、アプリケーションからデータベースに接続できるようにします。

必要なimport文の追加とデータベースの作成

main.py を開いてimport文を記述している箇所の一番下に以下のコードを追加します。

import文の直後にある models.Base.metadata.create_all(bind=engine) を記述することで接続先のデータベース上に今回使用するテーブルが存在しない場合は新規作成されます。

...
import schemes

import crud
import models
from database import SessionLocal, engine
from sqlalchemy.orm import Session

models.Base.metadata.create_all(bind=engine)

app = FastAPI(
...

自動保存、サーバーが再起動されると database.db というファイルが新規作成されます。このファイルが今回使用するsqliteのデータベースになります。

APIのリクエスト時に使う関数を用意

続けて main.py でロギングの定義をしている logger = logging.getLogger('uvicorn') の下に以下の関数を追加します。

この関数をAPI単位で呼び出すようにすることでAPIのリクエスト毎にデータベースのセッションを作成、接続を確立し、リクエストが終わるとセッションを閉じるようにしています。

...
logger = logging.getLogger('uvicorn')

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
...

データベースの中身を確認する

コマンドラインで確認

ここまででテーブル操作をするための下準備をしてきましたが、APIの実装に入る前にデータベースの中身を確認する方法を紹介します。SQLiteは以下のコマンドを実行することでSQLiteのクライアントでDBに接続します。

sqlite3 database.db

接続後、以下のSQLを実行することでテーブルの中身を確認できます。

SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
sqlite> SELECT * FROM entries ;
1|Title|First entry|undefined|2024-05-03 12:47:27

エディタの拡張機能を使う

SQLiteの中身を確認するときにはコマンドラインで接続する方法がメインですが、VSCodeの拡張機能を使って確認する方法があります。

エディタで拡張機能の管理画面を Ctrl + Shift + x (Macの場合は Cmd + Shift + x) で開いて検索窓から qwtel.sqlite-viewer を検索してトップに出てくるSQLite Viewerをインストールします。

インストール後、エディタで database.db を選択してSQLite Viewerを使って開くと直感的にデータの中身を確認できます。

APIでテーブルを操作できるようにする

POST /entry

ここから実際にAPIのリクエストがあった時に行うデータベースの処理を追加していきます。まずは、新規投稿を追加する処理です。

main.py の関数 create_entry を以下の関数に書き換えます。

async def create_entry(entry: schemes.EntryRequest, db: Session = Depends(get_db)):
    logger.info(f"Received entry: {entry}")
    crud.create_entry(db, entry)
    return {"message": "Entry created"}

自動保存、サーバーが再起動されると更新したAPIが使える状態になります。Step2で紹介したAPIドキュメントの中の「POST /entry」の項目のトグルを開いて「Try it out」ボタンをクリックして「Execute」ボタンをクリックすると、リクエストが実行されます。

実行後、以下の通りサーバーレスポンスのコードが「200」を返したらリクエストは正常に完了しています。もし、ステータスコードが「500」の場合はサーバー側でエラーが出ているため、サーバーを起動しているターミナルをご確認ください。

DELETE /entry

続いて投稿を削除する処理です。

main.py の関数 delete_entry を以下の関数に書き換えます。

async def delete_entry(entry_id: int, db: Session = Depends(get_db)):
    try: 
        crud.delete_entry(db, entry_id)
    except AttributeError:
        logger.error(f"Entry {entry_id} not found")
        raise HTTPException(status_code=404, detail=f"Entry {entry_id} not found")
    logger.info(f"Entry {entry_id} deleted")
    return {"message": f"Entry {entry_id} deleted"}

自動保存、サーバーが再起動されると更新したAPIが使える状態になります。Step2で紹介したAPIドキュメントの中の「DELETE /entry」の項目のトグルを開いて「Try it out」ボタンをクリックしたら、投稿IDを入力します。今回使用するテーブルのIDは追加された順にインクリメントされるので、試しに 1 を入力します。

IDを入力したら「Execute」ボタンをクリックすると、リクエストが実行されます。

以下の通りサーバーからのレスポンスが返ってきたらサーバーは正常に動作しています。

まとめ

このステップではデータベースをFastAPIで扱う方法を紹介しました。Djangoとは違って自分で実装する箇所が多いため、多少手間ですが公式ドキュメントでもこの手順がチュートリアルでも紹介されているので実装に困ることはないと思います。

また、今回は取り上げませんがDjangoのようなマイグレーションをしたい時には使用するORMに対応したマイグレーションツール(テーブル構成を更新するツールのこと)を導入する必要があります。

ここまででサーバーと通信をするためのAPIが用意できたので、最後のパートでは画面を作成して掲示板アプリをブラウザから使えるようにしましょう。

ここまででAPIは用意しましたが、最後にHTMLを用意してこれまで作成したAPIをつなぎこんだ掲示板アプリケーションを開発していきます。

なお、今回はあくまでFastAPIがメインなのでJavaScriptやCSSといったフロントエンド周りの話はあまり深く解説しません。

テンプレートエンジンとは?

ここではHTMLの表示にテンプレートエンジンを使います。

テンプレートエンジンとは、動的なコンテンツ生成に使用されるフレームワークやソフトウェアを指します。PythonのテンプレートエンジンだとJinja2が最も使われています。

FastAPIとよく似ているFlaskというフレームワークではこのJinja2がテンプレートエンジンとしてサポートされています。FastAPIでは任意のテンプレートエンジンを使うことができますが、直接Jinja2が使えるようにサポートされているので今回はJinja2をつかっていきます。

ページを用意する

テンプレートエンジンについて紹介したところで、HTMLを作成します。

以下のコマンドでHTMLの保存先、そして表示に使うHTMLファイルを新規作成します。

mkdir -p /workspace/gitpod-python-playground/templates
touch /workspace/gitpod-python-playground/templates/entry.html

templates/entry.html を開き以下のコードを書き込みます。

<!-- templates/entry.html -->
<!DOCTYPE html>
<html>

<head>
  <title>Py Channel</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
  <link href="https://use.fontawesome.com/releases/v6.2.0/css/all.css" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/axios@1.6.7/dist/axios.min.js"></script>
</head>

<body>
  <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
    <div class="container-fluid">
      <a class="navbar-brand" href="/">Py Channel</a>
    </div>
  </nav>
  <div class="container mb-4">
    <form id="entry-form">
      <div class="mb-3">
        <label for="title" class="form-label">Title</label>
        <input type="text" class="form-control" id="title" name="title">
      </div>
      <div class="mb-3">
        <label for="content" class="form-label">Content</label>
        <textarea class="form-control" id="content" name="content"></textarea>
      </div>
      <div class="mb-3">
        <label for="image" class="form-label">Name</label>
        <input type="text" class="form-control" id="user_name" name="user_name">
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </div>
  <div class="nav justify-content-center">
    {% for entry in entries %}
    <div class="card w-75">
      <div class="card-body">
        <h5 class="card-title">{{ entry.title }}</h5>
        <p class="card-text">{{ entry.content }}</p>
        <div class="row row-cols-lg-auto g-3 float-end">
          <div class="col-12">
            <p class="card-text"><small class="text-muted">{{ entry.user_name }} - {{ entry.created_at }}</small></p>
          </div>
          <div class="col-12">
            <i class="fa-solid fa-trash" name="delete" id="{{ entry.id }}" onclick="deleteEntry()"></i>
          </div>
        </div>
      </div>
    </div>
    {% endfor %}
  </div>
</body>
<script>
  const form = document.getElementById('entry-form');
  form.addEventListener('submit', async (event) => {
    event.preventDefault();
    const title = document.getElementById('title').value;
    const content = document.getElementById('content').value;
    const user_name = document.getElementById('user_name').value;
    const response = await axios.post('/entry', {
      title,
      content,
      user_name
    });
    if (response.status === 200) {
      window.location.reload();
    }
  });
  
  function deleteEntry() {
    const id = event.target.id;
    axios.delete(`/entry/${id}`).then((response) => {
      if (response.status === 200) {
        window.location.reload();
      }
    });
  }
</script>

</html>

サーバーで表示できるようにする

HTMLの用意ができたところでサーバーで必要なデータを取得、画面表示されるように実装していきます。

main.py を開き、import文に以下の1行を追加します。

from fastapi.templating import Jinja2Templates

同じく main.py のロギングを定義している箇所の下に以下の1行を加えます。

...
logger = logging.getLogger('uvicorn')
templates = Jinja2Templates(directory="templates")
...

続けて main.py の中の root 関数を以下の関数に書き換えます。

async def root(request: Request, db: Session = Depends(get_db)):
    entries = crud.get_entries(db)
    return templates.TemplateResponse(request=request, name="entry.html", context={'entries': entries})

自動保存、サーバーの再起動後、GitpodのサーバーURLをそのままブラウザで開きます。するとStep1ではJSONが表示されていたのが、以下のようなWebページを表示するように挙動が変わったことが確認できます。

試しに画面上部のフォームに掲示板のタイトル、本文、名前を記入すると以下の通り投稿一覧に送信した内容が表示されることが確認できます。

投稿の右下にあるゴミ箱アイコンをクリックすると該当の投稿が削除されるようになります。

今回のハンズオンのソースコード

ハンズオンの全体のソースコードは以下のGithubのレポジトリで公開しています。今回作ったアプリケーションをローカルで動かすためのDocker環境も用意しているのでぜひクローンしてお使いいただけると嬉しいです。そして、このハンズオンが良かったと思ってもらえて、Githubのアカウントをお持ちの方はぜひStarをつけてもらえると今後のモチベーションにつながるのでぜひよろしくお願いします。

https://github.com/Miura55/fastapi_tutorial

まとめ

ここまで実装してきたAPIを使用してブラウザで動作するWebアプリケーションが出来上がりました。

今回紹介したようにテンプレートエンジンを使うことでサーバーで取得したデータを画面に直接表示させることが簡単にできます。

近年だとフロントエンドフレームワークであるVue.jsやReactを使ったWebページが多く存在しますが、FastAPIはこれらのフレームワークを使って作成したアプリケーションのサーバーサイドとして活用する時には相性がいいです。

FastAPIを活用したアプリケーションのサンプルは公式ドキュメントでも紹介されているので、本格的にFastAPIを使ってみたい方はぜひ参考にしていただけると良いかと思います。

https://fastapi.tiangolo.com/ja/project-generation/#-

ここまでできたら最低限動くアプリは完成です。次のステップでは早くハンズオンが終わった方向けに今回開発したアプリケーションを更に改良するためのアイデアをいくつか紹介します。

ここまでハンズオンの手順は以上ですが、ここではいくつか今回開発したアプリケーションを改良していくためのアイデアを紹介します。

詳細な手順はあえて割愛しますが、実現するためのヒント、参考にすべき情報は紹介するので参考にしていただけると幸いです。

データベースのマイグレーション

今回使用したSQL AlchemyはデータベースのORMとしての役割がありますが、Step3でも言及した通りすでに作成したテーブルの構成を更新するときにはマイグレーションツールを導入する必要があります。このあと紹介する機能のアイデアの中には一部新たにテーブルを追加したり構成を変更する必要もあるので応用課題に挑戦する方は、先に取り組むことをおすすめします。

SQLAlchemy用のマイグレーションツールとしてはAlembicというツールが使われます。

https://alembic.sqlalchemy.org/en/latest/

具体的な導入手順としては以下のブログが参考になります。ブログではMySQLを接続する例ですが、接続先を変えればSQLiteでも実現可能です。

https://www.sria.co.jp/blog/2021/06/5557/

リアクション機能を用意する

リアクションはXなどのSNSでよくある機能です。この機能を追加するには、Step3のテーブル設計に新たに likes のようなカラムを追加してみると簡単に実現できるかと思います。

カラム名

キー

デフォルト

備考

id

int

primary

-

投稿ID

title

str

-

-

タイトル

content

str

-

-

本文

user_name

str

-

-

ユーザー名

likes

int

-

0

ユーザーのリアクション数(新規追加)

created_at

datetime

-

-

投稿日時

あとは、 PATCH /entry/{entry_id} でリクエストを実行することで likes の数字を更新する形でデータを記録するといいでしょう。

あとは、HTMLで表示する時はリアクション数を表示する項目を追加すればリアクション機能を簡易的に実装できると思います。

リプライを実装する

SNSだとメッセージに対して他の人がメッセージを投げるような処理もあるかと思います。

リプライのメッセージについてはタイトルを入れる必要性はないと思うので、リプライ用に以下の構造のテーブル replies を用意すると整理しやすいかと思います。

カラム名

キー

備考

id

int

primary

リプライID

entry_id

int

-

投稿ID(entriesと紐づいている)

content

str

-

本文

user_name

str

-

ユーザー名

created_at

datetime

-

投稿日時

APIとしては GET /entry/{entry_id} を追加することで投稿に紐づくメッセージを取得することができて画面描画も簡単になると思います。

もしリアクション機能を追加する時は上記のテーブルに likes カラムを追加すると良いです。

ハンズオンの内容は以上になります。お疲れ様でした!

次のページでは削除の手順を紹介していきます。

Gitpodのインスタンスの削除

Gitpodはフリープランであればタイムアウトでインスタンスが止まるだけで請求は来ないのでハンズオンの復習をするために残してもいいですし、削除したい場合は https://gitpod.io/workspaces から削除することができます。

無料枠だとインスタンスを立ち上げて作業できる時間の限度が合計50時間になります。