🌊

React(Typescript) + Typescript(Express) + MySQLでTodoリスト作成

2023/08/04に公開

この記事は基本的な部分の、CRUD操作をフロントからバックエンド、データベースを一通り実装してみる記事になっています。
つたない部分もあるのでその際は優しく教えていただければ幸いです。またわかりにくい部分なども教えていただければ改善していきたいと思います。

typescriptの環境構築

まずはバックエンド側の構築から始めます。
フロントから始めても、バックエンドから始めても特に問題はありません。
好きなほうから初めていただいて問題はありませんが、自分はバックエンド側の構築から始めていきます。
好きな階層に任意のフォルダを作成してから始めましょう。

npm init -y
npm i express
npm i -D typescript @types/node ts-node @types/express nodemon
npx tsc --init
touch index.ts

必要なものをインストールしてファイルの作成を行います。

index.ts
import express, { Application, Request, Response } from "express";

const app: Application = express();
const PORT = 3000;

try {
  app.listen(PORT, () => {
    console.log(`server running at://localhost:${PORT}`);
  });
} catch (e) {
  if (e instanceof Error) {
    console.error(e.message);
  }
}
package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon index.ts"  // 追加
  },

ここまで記述ができたら、サーバーを起動させてみましょう。

npm start

ターミナルに
server running at://localhost:3000
が出力されるか確認して、出力されていればサーバーが起動しています。

GETメソッドの作成

todoの一覧を返すAPIを作成します。
まずはフロントからGETリクエストがあったときに文字列を返して、リクエストレスポンスができるか確かめてみましょう。

index.ts
const app: Application = express();
const PORT = 3000;

app.get('/', (req: Request, res: Response) => {
  console.log("getリクエストを受け付けました。");
  return res.status(200).json({ message: "hello world" });
})

try{

res.status(200)で正常にリクエストが来てレスポンスを返せたことを表しています。
バックエンドのgetメソッドでリクエストが来た時にはhello worldを返す実装を行ったので、次はフロントからリクエストを投げて、hello worldが返ってくるか確認してみましょう。

Reactの環境構築

npx create-react-appのコマンドを使用してもいいのですが、viteのほうが早く使用できるようになるので今回はこちらを使います。
詳しい詳細は公式等で確認いただければと思います。

npm create vite@latest
  1. Project name: 任意の名前
  2. Select a framework: React
  3. Select a variant: Typescript
    選択が終わると正常に終了した場合、
Done. Now run:
  cd frontend(任意で設定したファイル名)
  npm install
  npm run dev

こちらが表示されているので、表示されている順にコマンドを実行してください。
サーバーが立ち上がったらターミナルに表示されている
Local: http://localhost:5173/
URLにアクセスしましょう。
vite

表示されていることが確認されたら、バックエンド側にリクエストを投げてhello worldが返ってくるか確認します。
今回はaxiosを使用しますので、axiosをインストールします。

npm install axios 
App.tsx
import axios from "axios";
import { useEffect } from "react";

export default function Home() {
  useEffect(() => {
    axios
      .get("http://localhost:3000")  // ローカルのバックエンドサーバーのURLにgetメソッドでアクセス
      .then((response) => {
        console.log(response.data.message)
      })
      .catch((e) => {
        console.log(e.message);
      });
  }, []);

  return <div></div>;
}

フロント側でリロードを行えば検証ツールのコンソールでバックエンドからのレスポンスを確認します。
ただ今のままではCORSのエラーというのが出ているはずです。
cors error

このエラーはフロント側とバックエンド側でORIGINが異なるところにアクセスしているため出ているエラーです。
バックエンド:localhost:3001
フロントエンド:localhost:5173
バックエンド側でこれを許容してあげる必要があります。

まずはパッケージをインストールしましょう。

npm install cors
npm install -D @types/cors  // typescriptを使用している場合は必要です
index.ts
import cors from "cors";

const app: Application = express();
const PORT = 3000;

app.use(cors({ origin: "http://localhost:5173" }));

corsの設定が終わったらフロント側で再度リフレッシュしてみましょう。
コンソールを確認すると、hello worldが確認できるはずです。
hello worldが確認できたら、この後実際にDBからデータをとってきた形をべた書きにして返してフロント側で表示させてみましょう。

index.ts
app.get("/", (req: Request, res: Response) => {
  console.log("getリクエストを受け付けました。");
  const todos = [
      {id: "id1", todo: "test1"},
      {id: "id2", todo: "test2"},
      {id: "id3", todo: "test3"},
      {id: "id4", todo: "test4"},
    ]

  return res.status(200).json({ todos });
});

フロント側で、取得したtodoを表示させるように実装してみましょう。

App.tsx
import { useEffect, useState } from "react";
import "./App.css";
import axios from "axios";

type TodoTypes = {
  id: string;
  todo: string;
};

function App() {
  const [todos, setTodos] = useState<TodoTypes[]>([]);

  useEffect(() => {
    axios.get("http://localhost:3000").then((response) => {
      console.log(response.data.todos);
      const { todos } = response.data;
      setTodos(todos);
    });
  }, []);

  return (
    <>
      {todos.map((todo) => (
        <p key={todo.id}>{todo.todo}</p>
      ))}
    </>
  );
}

export default App;

ここまでの実装が終わったら、画面にtest1 ~ test4までが表示されているはずです。

Todoを追加できるようにしよう

まずはユニークなIDを作成しているライブラリをインストールします。この後作成する、更新や削除時にユニークなIDが必要になってきます。

npm install uid
index.ts
import cors from "cors";
import { uid } from "uid"; // 追加

const app: Application = express();
const PORT = 3000;

app.use(cors({ origin: "http://localhost:5173" }));
app.use(express.json()); // 追加
app.use(express.urlencoded({ extended: true })); // 追加

今回追加したコードはフロントから送られてきたjsonデータをバックエンドで利用できるようにするためです。

index.ts
app.post("/add", (req: Request, res: Response) => {
  console.log("postリクエストを受け付けました。");
  console.log(req.body.data.todo);
  const { todo } = req.body.data;
  const uidValue = uid();
  return res.status(200).json({ id: uidValue, todo });
});

URLのパスがaddでメソッドがPOSTの際に実装した部分の処理が実行されます。
入力されたtodoをコンソールに表示させ、idと一緒にフロント側に返しています。

次はフロントからtodoを入力してidと一緒にデータを受け取れるか確認していきます。
ここでformからのデータ簡単に使えるようにライブラリをインストールしていきます。

npm install react-hook-form

使用する理由として、入力されたデータを取得しやすくするために使用しています。また、入力されたデータをバリデーションしたり簡単にエラーを表示したりできるので気になった人はぜひ公式も確認してみてください。
https://react-hook-form.com/

App.tsx
import { useEffect, useState } from "react";
import "./App.css";
import axios from "axios";
import { useForm } from "react-hook-form";

type TodoTypes = {
  id: string;
  todo: string;
};

type AddTodoType = {
  todo: string;
};

function App() {
  const { register, handleSubmit } = useForm<AddTodoType>();
  const [todos, setTodos] = useState<TodoTypes[]>([]);

  const addTodo = async (event: AddTodoType) => {
    const { todo } = event;
    console.log(todo);
    await axios
      .post("http://localhost:3000/add", {
        data: {
          todo,
        },
      })
      .then((response) => {
        console.log(response.data);
        const todo = response.data;
        setTodos((preTodos) => [todo, ...preTodos]);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  useEffect(() => {
    axios.get("http://localhost:3000").then((response) => {
      console.log(response.data.todos);
      const { todos } = response.data;
      setTodos(todos);
    });
  }, []);

  return (
    <>
      <form onSubmit={handleSubmit(addTodo)}>
        <input {...register("todo")} type="text" />
        <button type="submit">add</button>
      </form>
      {todos.map((todo) => (
        <p key={todo.id}>{todo.todo}</p>
      ))}
    </>
  );
}

export default App;

フロントで入力されたtodoがaddボタンを押すことでサーバー側にデータが送信され、画面に入力したtodoが新しく追加されたかと思います。
また、コンソール画面を見てみるとバックエンドで追加したidも確認できているかと思います。

次は削除機能を実装してみましょう

index.ts
app.delete("/delete", (req: Request, res: Response) => {
  console.log("deleteリクエストを受け付けました。");
  console.log(req.body.id);
  return res.status(200).json({ message: "success" });
});

URLのパスがdeleteでメソッドがdeleteの際に実行されます。
コンソールに削除するtodoのidを表示しています。

フロントエンド

App.tsx
import { useEffect, useState } from "react";
import "./App.css";
import axios from "axios";
import { useForm } from "react-hook-form";

type TodoTypes = {
  id: string;
  todo: string;
};

type AddTodoType = {
  todo: string;
};

function App() {
  const { register, handleSubmit } = useForm<AddTodoType>();
  const [todos, setTodos] = useState<TodoTypes[]>([]);

  const addTodo = async (event: AddTodoType) => {
    const { todo } = event;
    console.log(todo);
    await axios
      .post("http://localhost:3000/add", {
        data: {
          todo,
        },
      })
      .then((response) => {
        console.log(response.data);
        const todo = response.data;
        setTodos((preTodos) => [todo, ...preTodos]);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  const deleteTodo = async (id: string) => {
    console.log(id);

    await axios
      .delete("http://localhost:3000/delete", {
        data: {
          id,
        },
      })
      .then((response) => {
        console.log(response);
        const newTodos = todos.filter((todo) => todo.id !== id);
        setTodos(newTodos);
      })
      .catch((e) => {
        console.log(e.message);
        setTodos(todos);
      });
  };

  useEffect(() => {
    axios.get("http://localhost:3000").then((response) => {
      console.log(response.data.todos);
      const { todos } = response.data;
      setTodos(todos);
    });
  }, []);

  return (
    <>
      <form onSubmit={handleSubmit(addTodo)}>
        <input {...register("todo")} type="text" />
        <button type="submit">add</button>
      </form>
      {todos.map((todo) => (
        <div key={todo.id} style={{ display: "flex" }}>
          <p>{todo.todo}</p>
          <button onClick={() => deleteTodo(todo.id)}>delete</button>
        </div>
      ))}
    </>
  );
}

export default App;

todoの横にボタンを配置して、そのボタンが押されたときにバックエンド側にidを送っています。フロント側でも、リクエストが正常に終了した際にはtodoが消えているかと思います。

次はtodoを更新できるようにしていきましょう。

index.ts
app.put("/update", (req: Request, res: Response) => {
  console.log("putリクエストを受け付けました。");
  console.log(req.body.data);
  const { id, todo } = req.body.data;
  return res.status(200).json({ id, todo });
});

URLのパスがupdate、メソッドがputの場合に実行されます。
ターミナルで更新したい、idとtodoを確認できます。
処理としては、更新したい、idとtodoをそのまま返却しています。

フロントエンド側の実装

App.tsx
import { useEffect, useState } from "react";
import "./App.css";
import axios from "axios";
import { useForm } from "react-hook-form";

type TodoTypes = {
  id: string;
  todo: string;
};

type AddTodoType = {
  todo: string;
  editTodoName: string;
};

function App() {
  const { register, handleSubmit, reset } = useForm<AddTodoType>();
  const [todos, setTodos] = useState<TodoTypes[]>([]);
  const [isEdit, setIsEdit] = useState({ id: "", todo: "" });

  const addTodo = async (event: AddTodoType) => {
    const { todo } = event;
    console.log(todo);
    await axios
      .post("http://localhost:3000/add", {
        data: {
          todo,
        },
      })
      .then((response) => {
        console.log(response.data);
        const todo = response.data;
        setTodos((preTodos) => [todo, ...preTodos]);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  const deleteTodo = async (id: string) => {
    console.log(id);

    await axios
      .delete("http://localhost:3000/delete", {
        data: {
          id,
        },
      })
      .then((response) => {
        console.log(response);
        const newTodos = todos.filter((todo) => todo.id !== id);
        setTodos(newTodos);
      })
      .catch((e) => {
        console.log(e.message);
        setTodos(todos);
      });
  };

  const editTodo = async ({ editTodoName }: AddTodoType) => {
    await axios
      .put("http://localhost:3000/update", {
        data: {
          id: isEdit.id,
          todo: editTodoName,
        },
      })
      .then((response) => {
        console.log(response.data);
        const newTodos = todos.map((todo) => {
          return todo.id === response.data.id ? response.data : todo;
        });
        setIsEdit({ id: "", todo: "" });
        setTodos(newTodos);
        reset();
      })
      .catch((e) => {
        console.log(e.message);
      });
  };

  useEffect(() => {
    axios.get("http://localhost:3000").then((response) => {
      console.log(response.data.todos);
      const { todos } = response.data;
      setTodos(todos);
    });
  }, []);

  return (
    <>
      <form onSubmit={handleSubmit(addTodo)}>
        <input {...register("todo")} type="text" />
        <button type="submit">add</button>
      </form>
      {todos.map((todo) => (
        <div key={todo.id} style={{ display: "flex" }}>
          {isEdit.id === todo.id ? (
            <form onSubmit={handleSubmit(editTodo)}>
              <input {...register("editTodoName")} type="text" />
              <button>send</button>
            </form>
          ) : (
            <>
              <p>{todo.todo}</p>
              <button
                onClick={() => setIsEdit({ id: todo.id, todo: todo.todo })}
              >
                edit
              </button>
            </>
          )}

          <button onClick={() => deleteTodo(todo.id)}>delete</button>
        </div>
      ))}
    </>
  );
}

export default App;

editのボタンを押したらtodoが変更できるように、テキストフィールドを表示させ、入力できるようにしています。editのボタンをsendに変更してsendのボタン時にサーバー側にtodoとidを送信しています。

MySQL

一旦ここまででCRUD処理が全部できたかと思います。UIはちょっとあれですが、いったん機能的な部分としては問題ないかと思います。
続いてはMySQLを使用して行きましょう。
MySQLをインストールしていない方はインストールから行っていきましょう。
すでにインストール済みのかたは飛ばしてMySQLにログインから行っていきましょう。

MySQLのインストール方法はこちらを参考にしていただければと思います。
https://medium-company.com/mysqlのインストール手順/

また、MySQLのコマンドがわかりやすく乗っているサイトです。今回使用するコマンド以外にもたくさんあるので、こちらで確認するのもいいかと思います。
https://www.javadrive.jp/mysql/

ログイン

mysql -u root -p

Enter passwordと表示されるので、mysqlのパスワードを入力しましょう。
ログインに成功すると

mysql>

と表示されます。

データベースの作成

mysql> create database todos;

データベース一覧の取得

データベースが作られているか確認してみましょう。

mysql> show databases;

デフォルトで設定されているものあるので、自分が作ったデータベースがあるか確認してみましょう。

使用するデータベースの選択

mysql> use todos;
Database changed

テーブルの作成

mysql> create table todos.todo (id varchar(50), todo varchar(50));

テーブルが作成されているか確認

mysql> show tables;

テーブルが作成されたらいくつかデータを入れていきましょう

mysql> insert into todo values ("id1", "task1");
Query OK, 1 row affected // データが格納できたことを示します。

今回テーブル作成時にid,todoの順番で作成したので、データを入れる際もこの順番は守りましょう。

入れたデータが格納されているか確認していきましょう。

mysql> SELECT * FROM todos.todo;
+------+-------+
| id   | todo  |
+------+-------+
| id1  | task1 |
| id2  | task2 |
| id3  | task3 |
+------+-------+

このような形になっていればデータが格納されていることを示しています。
データも格納できたので、バックエンド側でMySQLにアクセスしてデータを取得してフロント側に返してみましょう。

ライブラリインストール

まずは、MySQLと接続するためのライブラリをインストールしましょう。
この後は基本的にはフロント側は実装することなくバックエンド側も、現在あるコードをデータベースとの連携をとることで変更する部分の実装になってきます。

npm install mysql2
index.ts
import mysql from "mysql2";

const connection = mysql.createConnection({
    host: "localhost",
    user: "設定したユーザー(何も設定していなければrootです)",
    password: "設定したパスワード",
    database: "作成したデータベース名(私の場合はtodos)",
  });

データベースの情報を記載しておき、リクエストがあったときに使用できるように変数に格納しておきましょう。

GETメソッドの修正

getメソッドの記述を以下のように変更していきます。

index.ts
app.get("/", (req: Request, res: Response) => {
  console.log("getリクエストを受け付けました。");
  const sql = "SELECT * FROM todo";
  connection.query(sql, (error, result) => {
    if (error) {
      res.status(500).json({ message: error.message });
    } else {
      res.status(200).json({ todos: result });
    }
  });
});

MySQLに接続してSQLでデータベースに命令を出しエラーが出なければ、作ったテーブルに格納されているデータをすべて取得してフロント側に返しています。
ここまで実装が終わったら画面を更新してみましょう。
データベースに格納したデータが画面に表示されているはずです。

POSTメソッドの修正

次はPOSTメソッドの部分を変更していきましょう

index.ts
app.post("/add", (req: Request, res: Response) => {
  console.log("postリクエストを受け付けました。");
  const { todo } = req.body.data;
  const uidValue = uid();
  const sql = `INSERT INTO todo VALUES ("${uidValue}", "${todo}")`;
  connection.query(sql, (error, result) => {
    if (error) {
      console.log(error);
      return res.status(500).json({ message: "Failed to add todo" });
    }
    return res.status(200).json({ id: uidValue, todo });
  })
});

この部分でテーブルにデータを格納しています。

DELETEメソッドの修正

次はDELETEメソッドの部分を修正していきましょう。

index.ts
app.delete("/delete", (req: Request, res: Response) => {
  console.log("deleteリクエストを受け付けました。");
  console.log(req.body.id);
  const id = req.body.id;
  const sql = `DELETE FROM todo WHERE id = "${id}"`;
  connection.query(sql, (error) => {
    if (error) {
      res.status(500).json({ message: error.message });
    } else {
      res.status(200).json({ message: "success" });
    }
  });
});

削除するidを取得して、テーブルの中にあるidと一致するものを削除しています。

PUTメソッドの修正

次はPUTメソッドの部分を修正していきましょう。

index.ts
app.put("/update", (req: Request, res: Response) => {
  console.log("putリクエストを受け付けました。");
  console.log(req.body.data);
  const { id, todo } = req.body.data;
  const sql = `UPDATE todo SET todo="${todo}" WHERE id="${id}"`
  connection.query(sql, (error) => {
    if (error) {
      res.status(500).json({ message: error.message });
    } else {
      res.status(200).json({ id, todo });
    }
  });
});

条件にユニークのidを指定して、そのidと一致するtodoを入力された値に更新しています。

UIはちょっと微妙というかただ単にそのまま表示しただけですみません。
一旦、今回の目標であった、React + Typescript(Express) + MySQLでのTodoアプリは作成できたかと思います。

Discussion