React+Express+MongoDB CRUD アプリ

アプリケーションの構成および環境

前回、以下の記事でReact+Firebaseという構成で、CRUDアプリを作成しました。

https://www.tech-introduction.com/reactfirebase-crud%e3%82%a2%e3%83%97%e3%83%aa-%e3%81%ae%e9%96%8b%e7%99%ba/

今回は、この構成を

  • フロントエンド:React
  • バックエンド:Express
  • データベース:MongoDB

として、CRUDアプリを作成してみます。

今回は以下の環境で開発しました。

  • node: v14.15.3
  • mongoDB: “4.4.3”
目次

ディレクトリ構造

まず、全体のディレクトリを作成します。

mkdir react-express-mongo
cd react-express-mongo

Reactのソースコード

まず、create-react-appでreactアプリを作成します。

npx create-react-app react

そして、必要なライブラリをインストールします。

npm install react-router

次に、srcディレクトリを次のように変更します。

.
├── node_modules/
├── package-lock.json
├── package.json
├── public/
└── src
    ├── components/
    ├── constants/
    ├── format/
    ├── index.js
    ├── serviceWorker.js
    └── setupTests.js

まず、index.jsを次のように編集します。

import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);
serviceWorker.unregister();

component/App.jsは以下のようにします。

import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import * as ROUTES from "../constants/routes";
import Show from "./Show";
import Create from "./Create";
import Update from "./Update";

class App extends React.Component {
  render() {
    return (
      <h1>App Component</h1>
    );
  }
}
export default App;

次に、Create, Read, UpdateおよびDeleteを行うルートを定義するパスを定義します。

constants/routes.jsとして以下のようにします。

export const SHOW = '/';
export const CREATE = '/create';
export const UPDATE = '/update/:id';
export const BUILD_UPDATE = (id) => `/update/${id}`;

Read, Create, Update, Delete機能のコードは以下です。

まず、表示機能のShow.jsは以下です。

import React from "react";
import "../App.css";
import { Link } from "react-router-dom";
import { CREATE, BUILD_UPDATE } from "../constants/routes";
import _ from "lodash";
import axios from "axios";
import { dataFormat } from "../format/dateFormat";

class Show extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      tasks: {},
    };
  }

  componentDidMount() {
    axios.get("http://localhost:5000/api/todos").then((res) => {
      console.log(res.data);
      this.setState({
        tasks: res.data,
      });
    });
  }

  render() {
    return (
      <div>
        <h3>
          <Link to={CREATE}>タスク作成</Link>
        </h3>
        <table>
          <thead>
            <tr>
              <th>タイトル</th>
              <th>詳細</th>
              <th>期限</th>
            </tr>
          </thead>
          <tbody>
            {_.map(this.state.tasks, (value) => {
              return (
                <tr key={value._id}>
                  <td>
                    <Link
                      to={{ pathname: BUILD_UPDATE(value._id), state: value }}
                    >
                      {value.title}
                    </Link>
                  </td>
                  <td>{value.detail}</td>
                  <td>{dataFormat(value.deadline)}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    );
  }
}

export default Show;

次に、Create.jsは以下です。

import React from "react";
import { SHOW } from "../constants/routes";
import axios from "axios";

class Create extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: "",
      detail: "",
      deadline: "",
    };
    this.handleChange = this.handleChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({
      [event.target.name]: event.target.value,
    });
    console.log(this.state);
  }

  onSubmit() {
    axios
      .post("http://localhost:5000/api/todos", this.state)
      .then((res) => {
        console.log(res);
        this.setState({
          title: "",
          detail: "",
          deadline: "",
        });
        this.props.history.push(SHOW);
      })
      .catch((err) => {
        console.error(err);
      });
  }
  render() {
    return (
      <div>
        <h4>タスク作成</h4>
        <form>
          <table>
            <tbody>
              <tr>
                <td>タイトル:</td>
                <td>
                  <input
                    type="text"
                    name="title"
                    onChange={this.handleChange}
                  ></input>
                </td>
              </tr>
              <tr>
                <td>タスク名:</td>
                <td>
                  <input
                    type="text"
                    name="detail"
                    onChange={this.handleChange}
                  ></input>
                </td>
              </tr>
              <tr>
                <td>期限:</td>
                <td>
                  <input
                    type="date"
                    name="deadline"
                    onChange={this.handleChange}
                  ></input>
                </td>
              </tr>
            </tbody>
          </table>
        </form>
        <button type="submit" onClick={this.onSubmit}>
          タスク作成
        </button>
      </div>
    );
  }
}
export default Create;

そして、Update.jsは以下です。

import React from "react";
import { SHOW } from "../constants/routes";
import axios from "axios";
import { dataFormat } from "../format/dateFormat";

class Update extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: "",
      detail: "",
      deadline: "",
    };
    this.handleChange = this.handleChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.deleteTask = this.deleteTask.bind(this);
  }

  componentDidMount() {
    axios
      .get(`http://localhost:5000/api/todos/${this.props.match.params.id}`)
      .then((res) => {
        console.log(res.data);
        this.setState({
          ...res.data,
        });
      });
  }

  handleChange(event) {
    this.setState({
      [event.target.name]: event.target.value,
    });
  }

  onSubmit() {
    axios
      .patch(
        `http://localhost:5000/api/todos/${this.props.match.params.id}`,
        this.state
      )
      .then((res) => {
        console.log(res);
        this.setState({
          title: "",
          detail: "",
          deadline: "",
        });
        this.props.history.push(SHOW);
      })
      .catch((err) => {
        console.error(err);
      });
  }

  deleteTask() {
    axios
      .delete(`http://localhost:5000/api/todos/${this.props.match.params.id}`)
      .then((res) => {
        console.log(res.data);
        this.props.history.push(SHOW);
      })
      .catch((error) => {
        console.log("Remove failed:" + error.message);
      });
  }

  render() {
    return (
      <div>
        <h4>タスク更新・削除</h4>
        <form>
          <table>
            <tbody>
              <tr>
                <td>タイトル:</td>
                <td>
                  <input
                    type="text"
                    name="title"
                    value={this.state.title}
                    onChange={this.handleChange}
                  ></input>
                </td>
              </tr>
              <tr>
                <td>タスク名:</td>
                <td>
                  <input
                    type="text"
                    name="detail"
                    value={this.state.detail}
                    onChange={this.handleChange}
                  ></input>
                </td>
              </tr>
              <tr>
                <td>期限:</td>
                <td>
                  <input
                    type="date"
                    name="deadline"
                    value={dataFormat(this.state.deadline)}
                    onChange={this.handleChange}
                  ></input>
                </td>
              </tr>
            </tbody>
          </table>
        </form>
        <table>
          <tbody>
            <tr>
              <td>
                <button type="submit" onClick={this.onSubmit}>
                  タスク更新
                </button>
              </td>
              <td>
                <button onClick={this.deleteTask}>削除</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    );
  }
}
export default Update;

ここで、dateのフォーマットの関数として以下で定義します。

react/format/dataFormat.js

import moment from "moment";
export const dataFormat = (date) => {
  return moment(date).format("YYYY-MM-DD");
};

以上で、React側の実装は終了です。次にバックエンドの実装をしていきます。

Expressのソースコード

Expressを使用して、各APIを実装していきます。

仕様は以下です。

GET /api/todos/ :全てのTodoを取得

POST /api/todos/:Todoを登録

PATCH /api/todos/:id :指定したidのTodoを更新

Delete /api/todos/:id:指定したidのTodoを削除

以上の仕様を満たすapiをExpressで実装していきます。

まず、express用のディレクトリ作成します。

mkdir react-express-mongo/express
cd express

まず、今回のアプリに必要なパッケージをインストールします。

npm install express cors moment mongoose
const dbConfig = {
    URL: "mongodb://127.0.0.1:27017",
    DB_NAME: "test",
}
module.exports = dbConfig;

ここでは、データベース名としてtestを用いています。

mongodbを起動させた後、mongoshellを用いて、以下のコマンドでtestを使用するようにします。

$ mongo
$ use test

続いて、データを格納するオブジェクトのmodelの定義を行います。

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const TodoSchema = new Schema({

    title: String,
    detail: String,
    deadline: Date,
});

module.exports = mongoose.model('Todo', TodoSchema);

前回のfirebaseで作成したものと同様に、titile, detail はString, deadlineはDateとして定義します。

次に、実際にapiを実装するapp.jsを以下に示します。

const express = require("express");
const cors = require("cors");
const mongoose = require("mongoose");
const dbConfig = require("./config/dbConfig");
const Todo = require("./models/todo");

mongoose
  .connect(dbConfig.URL + "/" + dbConfig.DB_NAME, { useNewUrlParser: true })
  .then(() => {
    console.log("mongodb connection is ok!");
  })
  .catch((err) => {
    console.log("mongodb connection is error", err);
  });

const app = express();
app.use(express.json());
app.use(cors());

// ToDo一覧の取得
app.get("/api/todos", (req, res) => {
  Todo.find().then((todos) => {
    res.json(todos);
  });
});

// ToDoの新規登録
app.post("/api/todos", (req, res, next) => {
  const { title, detail, deadline } = req.body;
  console.log(req.body);
  if (typeof title !== "string" || !title) {
    const err = new Error("title is required");
    err.statusCode = 400;
    return next(err);
  }
  const aTodo = new Todo({
    title: title,
    detail: detail,
    deadline: new Date(deadline),
  });
  aTodo
    .save(() => {
      console.log("save success");
      res.status(201).end();
    })
    .catch((err) => {
      console.log("save failed", err);
      res.status(400).end();
    });
});

// 指定されたIDのToDoを取得
app.get("/api/todos/:id", (req, res, next) => {
  const targetId = req.params.id;
  Todo.findById(targetId)
    .then((todo) => {
      res.json(todo);
    })
    .catch((err) => {
      console.error("id is not found", err);
      res.status(400).end();
    });
});

// ToDoの削除
app.delete("/api/todos/:id", (req, res) => {
  const targetId = req.params.id;
  Todo.findByIdAndDelete(targetId)
    .then((result) => {
      console.log("delete success", result);
      res.status(201).end();
    })
    .catch((err) => {
      console.error("delete failed", err);
      res.status(400).end();
    });
});

// 指定されたIDのToDoを更新
app.patch("/api/todos/:id", (req, res, next) => {
  const targetId = req.params.id;
  const updatedTodo = req.body;
  Todo.findByIdAndUpdate(targetId, updatedTodo)
    .then((result) => {
      console.log("update success", result);
      res.status(201).end();
    })
    .catch((err) => {
      console.error("id is not found", err);
      res.status(400).end();
    });
});
// エラーハンドリングミドルウェア
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.statusCode || 500).json({ error: err.message });
});

app.listen(5000);

以上で、react, expressの実装が完了しました。

動作確認するためにreactディレクトリでnpm start, expressディレクトリでnode app.jsを実行すると動作確認することができます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次