python

FastAPI、React、MongoDBでの開発方法

FastAPIはpythonのフレームワークで、Flaskに似ています。

特徴としては、パフォーマンスの高さ・自動ドキュメントの作成などがあります。

公式ドキュメントが丁寧に纏まっていてチュートリアルを進めておけば大体の使い方は把握できます。

pythonをバックエンドとして使うのであれば、FastAPIかDjango Rest Framework(DRF)が候補に上がると思います。Flaskは新規に開発を行うのであればもう候補に上がることは少ないでしょう。

FastAPIとDRFの違いをいくつかあげるとすれば、軽さとサーバー系統です。
DRFはdjangoのライブラリ的な位置づけなので、多数の機能をもっておりAPIサーバーとしてのみを想定して実装するには機能過多になっています。もう一つがFastAPIはデフォルトでASGIをサポートしているのに対して、DRFはデフォルトでWSGIをサポート(3.0系からASGIもサポート)しています。
ASGIはWSGIの後継仕様であり、非同期で動作するように設計されています。

Reactはフロントエンドのデファクトスタンダードと言っても過言ではありません。Vueを使っているところもありますが、シェア的にはstackoverflowによるとReactが1位です。

https://insights.stackoverflow.com/survey/2020#technology-web-frameworks

MongoDBはNoSQLデータベースの一種です。MySQLなどのRDB系との違いは、MySQLが構文を利用してデーブルの全ての行に操作する必要があるのに対して、MongoDBはドキュメント型なので自由に構造を変更することができます。

イメージとしては下記のようなイメージで、JSON形式で入ってくることになります。

今回使用するファイル

以下のリポジトリに格納してあります

https://github.com/kohei-kubota/farmstack

環境の準備

MongoDBをクラウド版の方でセットアップしていきます。ローカルの方でも可能です

MongoDBのセットアップ

https://www.mongodb.com/

公式サイトにアクセスしてサインインしてください。アカウントがなければサインアップでアカウント作成する必要があります。

無料枠もあるので料金は気にしなくて大丈夫です。スペックは下記の通りです。

ログインが完了すると下記のようになります。

Build a Databaseをクリックして、データベースを作成します。

無料枠で行いたいので、一番右のSharedを選択します。

プロバイダを選択する画面に遷移するので、好きなプロバイダとリージョンを選びます。
今回はGCPとアメリカリージョンに決定します。
Create Clusterをクリックし、clusterが作成されるまで待ちます。

clusterの作成には時間がかかるので少し待ちます。
無事作成されるとこのような画面が表示されます

作成されたDBをバックエンドと通信させるための設定をしていきます。

左のメニューからDatabase Accessを選択し、Add New Database Userをクリックします。

任意の名前とパスワードを設定します。データベースユーザー権限がRead and write to any databaseになっていることを確認してAdd Userをクリックします。

無事ユーザーが追加されていることが確認できたら、

cluster一覧の画面に戻りConnectをクリックし、Allow Access from Anywhereを選択後、choose a connection methodを押して次に遷移します。

DRIVERをpythonに変更し、versionを3.11にしておきます。

データベースと接続するためのコードが表示されているので、コピーしておきます。

忘れてもまた表示できるので、必要になった時にコピーしても構いません。

最後にclusterの中に入り、データベースを作成しておきます。

Collectionsタブを選択し、Add My Own dataをクリックします。

collection名とdocument名を任意に設定しておきます。

今回はTodoリストを作成するので、下記のようにします。

以上で、MongoDBの設定は完了です。

バックエンドの作成

FastAPIのプロジェクトを作成していきます。

任意ディレクトリで、requirements.txtを作成

fastapi == 0.65.1
uvicorn == 0.14.0
motor == 2.4.0

今回使用するfastapi、webサーバーのuvicorn、mongoDBとやり取りするためのmotorを指定

DBとやりとりする為のドライバーはpymongoなどもありますが、ノンブロッキングI/Oであるmotorを使います。

ライブラリをインストールしていきます。

pip install -r requirements.txt

環境を汚したくない人は、anaconadaやpipenvなどの仮想環境を作成した上に構築してください

まずは、実行ファイルであるmain.pyを作成していきます。

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from database import (
  fetch_one_todo,
  fetch_all_todos,
  create_todo,
  update_todo,
  remove_todo,
)
from model import Todo

# App object
app = FastAPI()

origins = [
  'http://localhost:3000',
  'http://localhost',
  ]

app.add_middleware(
  CORSMiddleware,
  allow_origins=origins,
  allow_credentials=True,
  allow_methods=["*"],
  allow_headers=["*"],
)

@app.get("/")
def read_root():
  return {"Hello world"}

@app.get("/api/todo")
async def get_todo():
  response = await fetch_all_todos()
  return response

@app.get("/api/todo{title}", response_model=Todo)
async def get_todo_by_title(title):
  response = await fetch_one_todo(title)
  if response:
    return response
  raise HTTPException(404, f"there is no Todo item with this title {title}")

@app.post("/api/todo", response_model=Todo)
async def post_todo(todo:Todo):
  response = await create_todo(todo.dict())
  if response:
    return response
  raise HTTPException(400, "Sometheng went wrong / Bad Request")

@app.put("/api/todo{title}/", response_model=Todo)
async def put_todo(title:str, desc:str):
  response = await update_todo(title, desc)
  if response:
    return response
  raise HTTPException(404, f"there is no Todo item with this title {title}")

@app.delete("/api/todo{title}")
async def delete_todo(title):
  response = await remove_todo(title)
  if response:
    return "Successfully deleted todo item!"
  raise HTTPException(404, f"there is no Todo item with this title {title}")

詳細は省きますが、CRUD操作に必要なエンドポイントを用意しているのと、別ドメインからアクセスできるようにCORSを許可する設定を書いてあります。

サーバーを起動するには下記コマンドを実行
–reloadオプションをつけるとコードを変更した際に自動的にブラウザ側が更新されます

uvicorn main:app --reload

swaggerUIを確かめて見たい場合には、コードをシンプルにして確かめます。

URLがhttp://localhost:8000/docsになるので注意してください

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware

# App object
app = FastAPI()

origins = [
  'http://localhost:3000',
  'http://localhost',
  ]

app.add_middleware(
  CORSMiddleware,
  allow_origins=origins,
  allow_credentials=True,
  allow_methods=["*"],
  allow_headers=["*"],
)

@app.get("/")
def read_root():
  return {"Hello"}

次にモデルを作成していきます。

pydanticというデータベースモデルを定義するのに役立つので、これを使用していきます。

from pydantic import BaseModel

class Todo(BaseModel):
  title: str
  description: str

Djangoなどと似たような形で定義可能です。

最後にDBに対して操作する具体的な内容をdatabase.pyを作成していきます。

from model import Todo

# mongoDB driver
import motor.motor_asyncio

client = motor.motor_asyncio.AsyncIOMotorClient('mongodb+srv://<username>:<password>@cluster0.zbuzx.mongodb.net/myFirstDatabase?retryWrites=true&w=majority')
database = client.TodoList
collection = database.todo

async def fetch_one_todo(title):
  document = await collection.find_one({"title":title})
  return document

async def fetch_all_todos():
  todos = []
  cursor = collection.find({})
  async for document in cursor:
    todos.append(Todo(**document))
  return todos

async def create_todo(todo):
  document = todo
  result = await collection.insert_one(document)
  return document

async def update_todo(title, desc):
  await collection.update_one({"title":title}, {"$set": {
    'description': desc
  }})
  document = await collection.find_one({"title": title})
  return document

async def remove_todo(title):
  await collection.delete_one({"title":title})
  return True

以上でバックエンドの作成は完了です。swaggerUIは下記のようになります。

フロントエンドの作成

Reactを作成していくのですが、今回はcreate react appを使用していきます。

任意ディレクトリに移動した後に下記コマンドでreactプロジェクトを作成してください。

最後に.をつけることで、直下にプロジェクトを作成することができます

npx create-react-app .

フロント側のサーバー起動方法は下記コマンドで可能です

npm start

無事サーバーの起動まで確認できたら、必要なパッケージをインストールしていきます。

npm install axios bootstrap

axiosはバックエンドと通信するパッケージで、bootstrapはデザインのためのものです。

まずは、App.jsを変更していきます。

import "./App.css";
import React, { useState, useEffect } from "react";
import axios from "axios";
import "bootstrap/dist/css/bootstrap.min.css";
import TodoList from "./components/TodoList";

function App() {
  const [todoList, setTodoList] = useState([{}]);
  const [title, setTitle] = useState("");
  const [desc, setDesc] = useState("");

  // Read all Todos
  useEffect(() => {
    axios.get("http://localhost:8000/api/todo").then((res) => {
      setTodoList(res.data);
    });
  }, []);

  // Post a todo
  const addTodoHandler = () => {
    axios
      .post("http://localhost:8000/api/todo", {
        title: title,
        description: desc,
      })
      .then((res) => console.log(res));
  };

  return (
    <div
      className="App list-group-item justify-content-center align-items-center mx-auto"
      style={{ width: "400px", backgroundColor: "white", marginTop: "15px" }}
    >
      <h1
        className="card text-white bg-primary mb-1"
        stylename="max-width: 20rem;"
      >
        Task Manager
      </h1>
      <h6 className="card text-white bg-primary mb3">
        FASTAPI - React - MongoDB
      </h6>
      <div className="card-body">
        <h5 className="card text-white bg-dark mb-3">Add Your Task</h5>
        <span className="card-text">
          <input
            className="mb-2 form-control titleIn"
            placeholder="Title"
            onChange={(event) => setTitle(event.target.value)}
          />
          <input
            className="mb-2 form-control desIn"
            placeholder="Description"
            onChange={(event) => setDesc(event.target.value)}
          />
          <button
            className="btn btn-outline-primary mx-2 mb-3"
            style={{
              borderRadius: "50px",
              fontWeight: "bold",
            }}
            onClick={addTodoHandler}
          >
            Add Task
          </button>
        </span>

        <h5 className="card text-white bg-dark mb-3">Add Your Task</h5>
        <div>
          {/* Todo items - external component */}
          <TodoList todoList={todoList} />
        </div>
      </div>
      <h6 className="card text-dark bg-warning py-1 mb-0">
        Copyright 2021, All rights reserved &copy;
      </h6>
    </div>
  );
}

export default App;

新しく追加されたReact Hooksを一部使用していますが、難しいことはしていないので簡単に読めるかと思います。

続いてCSSも変更していきます。変更するファイルはApp.cssです。

.App {
  text-align: center;
  margin: 0;
  padding: 0;
  font-family: Arial, Helvetica, sans-serif;
}

ここからは、新規にコンポーネントを作成していきます。

src以下にcomponentsフォルダを作成し、components以下にTodo.jsとTodoList.jsファイルを作成しておきます。

まず、Todo.jsを作っていきます。

import React from "react";
import axios from "axios";

const Todo = (props) => {
  const deleteTodoHandler = (title) => {
    axios
      .delete(`http://localhost:8000/api/todo${title}`)
      .then((res) => console.log(res.data));
  };
  return (
    <div>
      <p>
        <span style={{ fontWeight: "bold, underline" }}>
          {props.todo.title} :
        </span>{" "}
        {props.todo.description}
        <button
          onClick={() => deleteTodoHandler(props.todo.title)}
          className="btn btn-outline-danger my-2 mx-2"
          style={{ borderRadius: "50px," }}
        >
          X
        </button>
      </p>
      <hr></hr>
    </div>
  );
};

export default Todo;

最後に一覧を表示するコンポーネントであるTodoList.jsを作成していきます。

import React from "react";
import Todo from "./Todo";

const TodoList = (props) => {
  return (
    <div>
      <ul>
        {props.todoList.map((todo, i) => (
          <Todo todo={todo} key={i} />
        ))}
      </ul>
    </div>
  );
};

export default TodoList;

以上でバックエンドと繋ぎ込むためのフロントエンドの作成が完了しました。

今回はサーバー側から取得してきた情報をstate管理していないので、自動的に再レンダリングされません。
なので、todoを作成した場合などはブラウザを更新する必要があります。

このような画面ができていたら成功です。