葛のメモ帳

自分で調べたことを忘れないためにメモっておきます

葛のメモ帳

自分で調べたことを忘れないためにメモっておきます


Webアプリのひな型みたいなのを作った[DockerCompose+React/TS/Vite+Python/FastAPI+MySQL]

前置き

最近はR&Dで働く葛です。お久しぶりです。

小さなWebアプリを小さく初めて、試作品をたくさん作りたいと思っているのにあんまりフレームワークがないなと思っていました。

なんでもいいのでFrontend, Backend, Databaseをフルカスタマイズ可能で小さくサクッと始めるために今回作ってみました。

やったこと

  • Frontend + Backend + Database のコンテナを立ててるDockerComposeを造りました。
  • Frontendは Vite + React + TypeScriptです。(これは viteでプロジェクト建てただけ) 
  • Backendは Pythonの FastAPIです。
  • Databaseは MySQLです。

環境

Windows11+WSL+Ubuntu 22.03です。

ディレクトリ構成

$ tree -L 2
.
├── backend
│   ├── Dockerfile
│   ├── README.md
│   ├── __pycache__
│   ├── config
│   ├── database
│   ├── main.py
│   ├── models
│   ├── requirements.txt
│   ├── routers
│   └── services
├── docker-compose.yml
├── docs
└── frontend
    ├── Dockerfile
    ├── README.md
    ├── index.html
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── public
    ├── src
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts

docker-compose.yml

以下はとりあえずローカルで適当に動かすためだけに作りました。SQLのパスワードなどを直書きしているので、実運用させたい場合は、環境変数化したりして適宜変更が必要だと思います。

services:
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    depends_on:
      - db
    volumes:
      - ./backend:/app
  db:
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: sample_password
      MYSQL_USER: sample_user
      MYSQL_PASSWORD: sample_user_password
  frontend:
    build: ./frontend
    ports:
      - "5173:5173"
    volumes:
      - ./frontend:/app

説明

  1. frontendは port 5173でlistenさせています。
  2. backendは port 8000でlistenさせています。
  3. databaseはport 3306で自動的にlistenされていました。
  4. それぞれマウントさせて、Dockerコンテナ内での開発内容をローカルに反映させています。

Dockerfile

Frontend

# Use an official Node.js image
FROM node:20

# Set the working directory in the container to /app
WORKDIR /app

# Add the current directory contents into the container at /app
ADD . /app

# Install any needed packages specified in package.json
RUN npm install

# Run npm start when the container launche
# CMD ["npm", "run", "dev"]
# 開発時に起動させるためだけのコマンド
CMD [ "tail", "-f", "/dev/null" ]

説明

  1. 最初にプロジェクトだけをviteで作るために、VSCodeの機能でdev containerを立てました。そのなかで viteを使ってプロジェクトを立ち上げました。
    • プロジェクトのソースコードだけを作るのはcontainer内でやる必要がなかったかも。
    • プロジェクトの作ったら、とりあえずコンテナを閉じました。
  2. CMDで npm run devをしてしまうと、開発中にコンテナを閉じたり建てたりを繰り返す必要が出てきます。これは非常に手間です。
    • そこで 最後の行を追加して、何もしない tail コマンドを実行させて待機させます。その間に vscodeの attach containerを使って起動中のコンテナに入ることで開発ができます。これが便利でした。

Backend

FROM python:3.11

WORKDIR /app

COPY . /app/

RUN pip install --no-cache-dir -r requirements.txt

# CMD [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload" ]
CMD [ "tail", "-f", "/dev/null" ]

ほぼやっていることは一緒です。

あれ、COPY っているんだっけ?(また調べます)

DatabaseとBackendの連携について

おそらくここが鬼門だったと思います。

最初にフレームワークの説明をします。

├── backend             
│   ├── config          : 固定値などを入れる
│   ├── database        : SQL Alchemey
│   ├── main.py         : エントリーポイント
│   ├── models          : PyDantic
│   ├── requirements.txt
│   ├── routers         : エンドポイント
│   └── services        : 業務ロジック
  • SQL ALchemyを利用してDatabaseを定義します。またSQLを操作します。
  • FastAPIでのデータのやりとりはPyDanticを利用します。
  • データの連結イメージは以下です
    • Database <-[SQL Alchemy]-> FastAPI <-[PyDantic]-> Frontend

main.py

# main.py

from fastapi import FastAPI
from routers.index_router import index_router
from routers.user_router import user_router
from models.base import Base
from database.database import engine

app = FastAPI()

@app.on_event("startup")
async def startup_event():
    Base.metadata.create_all(engine)

app.include_router(index_router)
app.include_router(user_router)

FASTAPI のエントリーポイントは上のように書きました。最初にFastAPIを初期化します。

database.py

# database.py
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "mysql://root:sample_password@db:3306"  # Link Docker Compose ENVIROMENT VARIABLE
DATABASE_NAME = "new_database"

# DB名を指定せずに接続する
engine = create_engine(f"{DATABASE_URL}")

# DBがなければ作成する
with engine.connect() as connection:
    connection.execute(text(f"CREATE DATABASE IF NOT EXISTS {DATABASE_NAME}"))

# 切断する
engine.dispose()

# 新しく作成したDBに再接続する
engine = create_engine(f"{DATABASE_URL}/{DATABASE_NAME}")

# SessionLocalクラスを作成
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

database.pyではDBの初期化やセッション、接続などの基本部分を書きました。

DATABASEとの接続する際はDocker Networkを使うのでそこに合わせて使います。

base.py

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

スキーマ定義のベース定義です。これを継承して各スキーマを定義していきます。

database.user_schema.py

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

from models.base import Base

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    email = Column(String(50))

Userというテーブルを作ります。

models.user_model.py

from pydantic import BaseModel

class UserBase(BaseModel):
    name: str
    email: str

class UserCreate(UserBase):
    name: str
    email: str

class UserRead(UserBase):
    id: int
    name: str
    email: str

class UserUpdate(UserBase):
    id: int
    name: str
    email: str

UserクラスにそれぞれCRUDに対応したクラスを定義しました。

これを使ってEndpointを定義していきます。

routers.user_router.py

# user_router.py
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session

from models.user_model import UserCreate, UserRead, UserUpdate
from database.database import SessionLocal
from database.user_schema import User as UserSchema

user_router = APIRouter()

# 依存関係
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@user_router.post("/users/", response_model=UserCreate)
def create_user(request_user: UserCreate, db: Session = Depends(get_db)):
    user_schema = UserSchema()
    user_schema.name = request_user.name
    user_schema.email = request_user.email
    db.add(user_schema)
    db.commit()
    db.refresh(user_schema)
    return user_schema

@user_router.get("/users/{user_id}", response_model=UserRead)
def read_user(user_id: int, db: Session = Depends(get_db)):
    user = db.query(UserSchema).filter(UserSchema.id == user_id).first()
    return user

@user_router.put("/users/{user_id}")
def update_user(user_id: int, user: UserUpdate, db: Session = Depends(get_db)):
    db_user = db.query(UserSchema).filter(UserSchema.id == user_id).first()
    db_user.name = user.name
    db_user.email = user.email
    db.commit()
    db.refresh(db_user)
    return db_user

@user_router.delete("/users/{user_id}")
def delete_user(user_id: int, db: Session = Depends(get_db)):
    user = db.query(UserSchema).filter(UserSchema.id == user_id).first()
    db.delete(user)
    db.commit()
    return {"message": "User deleted successfully"}

CRUDのエンドポイントを作ってみました。 それぞれ PyDanticで受け取ったデータをORMでSQL Alchemryに変換してDBとやりとりします。

以上で実装は終わりです。

CURLで動作確認してみる。

  • ユーザーの作成(POST /users/):
curl -X POST "http://localhost:8000/users/" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{\"name\":\"string\",\"email\":\"user@example.com\"}"
  • ユーザーの読み取り(GET /users/{user_id}):
curl -X GET "http://localhost:8000/users/1" -H  "accept: application/json"
  • ユーザーの更新(PUT /users/{user_id}):
curl -X PUT "http://localhost:8000/users/1" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{\"id\":1,\"name\":\"new_name\",\"email\":\"new_email@example.com\"}"
  • ユーザーの削除(DELETE /users/{user_id}):
curl -X DELETE "http://localhost:8000/users/1" -H  "accept: application/json"

DockerのSQLに入って確認してみる

MySQL Docker

docker compose run db mysql -h db -u root -psample_password

上は本来非推奨 パスワード直打ちだからです。

-h db オプションを追加したことで、MySQLクライアントはDocker Network経由でMySQLサーバーに接続しようとしました。これは、Docker環境では各コンテナが独自のネットワーク空間を持っているため、localhost127.0.0.1ではなく、サービス名(この場合はdb)を使用して他のコンテナに接続する必要があるからです。

あとは作成したDBを適宜確認すればOKです。

最後に

今回は上のようなものを作ってみました。

BACKENDとDBの骨子ができれば、あとはFrontendにUIを作って、JSONを描画させて、BACKENDに業務ロジックを実装するだけですね。

おそらくはもっと考慮すべき事項もあると思いますが、今回はこれくらいにします。

何か気になった指摘があればください。