現役エンジニアがNode.jsを解説! 〜フロントエンドから実装したAPIを使おう〜

公開日:2022-10-02
JavaScript
Node.js

https://www.youtube.com/watch?v=JLDASYSzu2Q

この講座はYouTubeで動画形式でも用意しています。合わせてご覧ください。

目標

フロントエンドから実装したAPIを使おう。

前回の説明の補足(SQLインジェクション対策)

app.post("/todo", (req, res) => {
  console.log(req.body);
  const todo = {
    status: req.body.status,
    task: req.body.task,
  };
});
connection.query(
  "INSERT INTO todo SET ?",
  todo,
    (error, results) => {
      if (error) {
        console.log(error);
        res.status(500).send("error");
        return;
    }
    res.send("ok");
  });

INSERT INTO todo SET ?の部分で?に渡す値がオブジェクトの場合、不正なデータを含むリクエストを受信することでSQLインジェクションができてしまうと分かったので訂正します。今回だとtodoという変数にstatustaskというプロパティをもつオブジェクトを渡してしまっています。
この問題についてコチラの記事も参考にしてみてください。

const connection = mysql.createConnection({
  host: "127.0.0.1",
  user: "root",
  database: "db",
  // 本番環境では環境変数を使うようにする
  password: "example",
  // SQLインジェクション対策
  stringifyObjects: true,
});

SQLインジェクション対策のためにcreateConnectionの中の
stringifyObjectsの設定がデフォルトだとfalseになっているのをtrueに設定します。
この場合、コードの書き方を変更する必要があります。

"INSERT INTO todo SET ?",
  todo,

上記の部分を次のように変更します。

"INSERT INTO todo (status, task) VALUES (?, ?)",
[todo.status, todo.task],

先程まではオブジェクトを?に渡していたのですが、
それを配列でオブジェクトのプロパティにそれぞれ渡しています。

コードの解説

要素の参照の作成

const ulRef = document.querySelector("ul");
const inputRef = document.querySelector("#todo-task");
const selectRef = document.querySelector("#todo-status");
const editItemIdRef = document.querySelector("#edit-item-id");
const submitButtonRef = document.querySelector("#submit-button");
const cancelButtonRef = document.querySelector("#cancel-button");
<div>
  <label id="item-label" for="item">Enter a item:</label>
  <input id="edit-item-id" type="hidden"  >
  <input type="text" name="item" id="todo-task">
  <select name="todo-status" id="todo-status">
    <option value=""></option>
    <option value="1">INCOMPLETE</option>
    <option value="2">PROGRESS</option>
    <option value="3">PENDING</option>
    <option value="4">COMPLETE</option>
  </select>
  <button id="submit-button">Add item</button>
  <button id="cancel-button">Cancel</button>
</div>

ステータス名に変換する関数

getStatusNameという関数では、
APIでstatusを取得した際にstatus情報は数字で返ってくるので
それらをわかりやすいように対応する文字列に変換する処理を行なっています。

const getStatusName = (status) =>
  status === 1
    ? "INCOMPLETE"
    : status === 2
    ? "PROGRESS"
    : status === 3
    ? "PENDING  "
    : "COMPLETE";

フォームのリセット処理

handleResetFormはinputやselectで選択されている値を初期化する関数で、
フォームのcancelボタンを押した際に行われる処理です。
他にも使いまわしているところがあるので関数で切り出しているような形になります。

const handleResetForm = () => {
  inputRef.value = "";
  selectRef.value = "";
  editItemIdRef.value = "";
  submitButtonRef.textContent = "Add Item";
};

アイテムを選択した際の処理

handleSelectEditItemは編集するアイテムを選択した際に実行される関数となっています。
フロント側のeditボタンを押した際に情報が反映される処理になります。

const handleSelectEditItem = ({ id, task, status }) => {
  editItemIdRef.value = id;
  inputRef.value = task;
  selectRef.value = status;
  submitButtonRef.textContent = "Edit Item";
};

TODOリストの取得と画面への反映(handleGetItems)

この辺から実際にAPIが絡んだ操作になります。
API通信は非同期処理になるのでasync/awaitで書いています。
fetch(`${url}/todo`)でGET/todoを実行して、todoアイテム一覧を取得しています。
また、res.json()ではfetchでデータを取得した際に、そのデータがJSONフォーマットで返ってくる場合は
jsonメソッドを実行してあげるとオブジェクトに変換される処理となります。

const handleGetItems = async () => {
  const res = await fetch(`${url}/todo`);
  const todos = await res.json();

TODOリストの取得と画面への反映 ( while (ulRef.firstChild) )

ulRef.removeChild(ulRef.firstChild)でリストの中にある項目を1回全部消す操作をしています。handleGetItemsでデータを取得。それをtodoリスト画面に反映させる処理となります。
そのため、アイテムを追加、編集して、もう一度データを取得し画面反映をする際に、前のリストが残っていると追加でリストが表示されてしまうので一旦全部消してからリストを追加する操作をしています。

  while (ulRef.firstChild) {
    ulRef.removeChild(ulRef.firstChild);
  }

TODOリストの取得と画面への反映 ( todo.forEach )

todos.forEach((todo)でtodoのアイテムを1個ずつ表示する処理を行なっています。

  inputRef.value = "";
  todos.forEach((todo) => {
    const spanRef = document.createElement("span");
    const liRef = document.createElement("li");
    spanRef.textContent = `${todo.task} - ${getStatusName(todo.status)}`;
    liRef.appendChild(spanRef);

spanとliの要素をdocument.createElementで作成します。

    const spanRef = document.createElement("span");
    const liRef = document.createElement("li");

spanRefのtextContentにtodoの内容を表示させています。
- ${getStatusName(todo.status)}ここにtodo.statusを引数で渡してあげて
PENDINGやCOMPLETEのような状態を作ってあげています。
それをtextContentに入れてあげて、liの要素にappendChildでspanを足してあげています。

spanRef.textContent = `${todo.task} - ${getStatusName(todo.status)}`;
liRef.appendChild(spanRef);

TODOリストの取得と画面への反映 ( editButtonRef )

ここではeditボタンの処理を行なっています。
editボタンを作成したところに、addEventListenerでボタンが押された際の処理を追加登録しています。
ここでアイテムを選択した際の処理であるhandleSelectEditItemの実行を登録しています。

    const editButtonRef = document.createElement("button");
    editButtonRef.textContent = "edit";
    liRef.appendChild(editButtonRef);
    editButtonRef.addEventListener("click", () =>
      handleSelectEditItem({
        id: todo.id,
        task: todo.task,
        status: todo.status,
      })
    );

TODOリストの取得と画面への反映 ( deleteButtonRef )

editボタンと同じようにdeleteボタンを生成し、
addEventListenerhandleDeleteItemを設定しています。

const deleteButtonRef = document.createElement("button");
    deleteButtonRef.textContent = "delete";
    liRef.appendChild(deleteButtonRef);
    deleteButtonRef.addEventListener("click", () => handleDeleteItem(todo.id));

    ulRef.appendChild(liRef);

TODOの登録

handleAddItemhandleEditItemが追加するAPIとの通信処理になります。

const handleAddItem = async () => {
  const value = inputRef.value;
  const status = selectRef.value;
  await fetch(`${url}/todo`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ task: value, status }),
  });
  await handleGetItems();
};

inputRefinputRefのvalueを取得して変数に入れています。

 const value = inputRef.value;
 const status = selectRef.value;

今回は登録になるのでPOSTメソッドでfetchを実行しています。
JSONでfetchを使ってAPIリクエストする際はheaderに"Content-Type": "application/json"が必要になるので追加しています。
続いてbodyに送るデータを渡すのですが、
オブジェクトをJSON文字列に変換する必要があるのでJSON.stringifyで変換しています。
非同期処理のため完了後、handleGetItemsを実行します。
ここでアイテムが登録された上で、もう一度データの再取得と画面の再描画をします。

TODOの更新

更新にはPUTメソッドを用います。
他の部分はTODOの登録と似た処理ですが、
違いとしてhandleEditItemではeditItemIdRefからidを取得しています。
editItemIdRefのvalueはhandleSelectEditItemを実行した際に選択した項目のidをeditItemIdRef.valueに渡しています。

const handleEditItem = async () => {
  const id = editItemIdRef.value;
  const task = inputRef.value;
  const status = selectRef.value;
  await fetch(`${url}/todo/${id}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ task, status }),
  });
  await handleGetItems();
};

TODOの削除

項目の削除はfetchでidを指定、DELETEメソッドで実行。
完了したらhandleGetItemsをまた実行する流れになります。

const handleDeleteItem = async (id) => {
  await fetch(`${url}/todo/${id}`, {
    method: "DELETE",
  });
  await handleGetItems();
};

登録ボタン押下時のイベント登録

画面上のEditItem、AddItemを押した時の処理になります。
これらは同じボタンになるのでsubmitボタンのtextContentがEditか否かで
handleEditItemhandleEditItemのどちらを実行するのか条件分岐を行なっています。
どちらかの登録処理の実行後、handleResetFormを呼ぶことによって、
編集、登録のどちらの場合でもフォームに入れていた内容がリセットされるようになっています。

submitButtonRef.addEventListener("click", async () => {
  if (submitButtonRef.textContent === "Edit Item") {
    await handleEditItem();
  } else {
    await handleAddItem();
  }
  handleResetForm();
});

キャンセルボタン押下時のイベント登録

先ほどまでは登録処理の実行後のフォームのリセットでしたが、
コチラではキャンセルボタンが押されたらフォームのリセットが行われる処理になります。

cancelButtonRef.addEventListener("click", handleResetForm);

まとめ

今回のコードのように共通処理はできるだけ括り出すようにすると、
デバッグや使い回しがしやすくなるのでおすすめです。

前回までの内容からフロントエンドの実装をすることによって、
Node.jsでアプリケーションを作るイメージがしやすくなったはずです。