Express.jsでTODOアプリケーションの作成を作成する方法

概要

Express v4.17.1
nodejs v19.7.0

前半ではExpress.jsを使用してAPIを構築し、後半ではHTMLとJavaScriptを使用してフロントエンドを作成することで、完全なTODOアプリケーションを作成します。

Express.js v4.17.1
Node.js v16.3.0

また、今回作成するコードは全て、GitHubに掲載しています。

  1. APIのエンドポイント:

  2. GET /todos すべての TODO を取得する

  3. POST /todos 新しい TODO を作成する

  4. PUT /todos/:id 既存の TODO を更新する

  5. DELETE /todos/:id 既存の TODO を削除する

  6. フロントエンドの機能:

  7. TODOのリストを表示する

  8. 新しいTODOを作成する

  9. 既存のTODOを更新する

  10. 既存のTODOを削除する

TODOアプリのバックエンドを作成する方法

server.js

// モジュールをインポートします
const express = require("express");
const app = express();
const path = require('path');

// HTMLファイルをホストするディレクトリを指定します。
app.use(express.static(path.join(__dirname, 'public')));

// 一覧用の Todo を作成します
let todos = [
  { id: 1, text: "野球をする", completed: false },
  { id: 2, text: "ゲームをする", completed: false },
  { id: 3, text: "仕事をする", completed: false },
];

app.use(express.json());

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// '/todos'パスで Todo の一覧を取得します
app.get("/todos", (req, res) => {
  res.json({ todos });
});

// "/todos"パスに POST リクエストを送信して Todo を新しく作成します
app.post("/todos", (req, res) => {
  const newTodo = req.body;
  newTodo.id = todos.length + 1;
  todos.push(newTodo);
  res.json({ todo: newTodo });
});

// "/todos/:id"パスに PUT リクエストを送信して Todo を更新します
app.put("/todos/:id", (req, res) => {
  const id = req.params.id;
  let todo = todos.find((t) => t.id == id);
  if (todo) {
    Object.assign(todo, req.body);
    res.json({ todo });
  } else {
    res.status(404).send({ message: "Todo が見つかりません" });
  }
});

// "/todos/:id"パスに DELETE リクエストを送信して Todo を削除します
app.delete("/todos/:id", (req, res) => {
  const id = req.params.id;
  const index = todos.findIndex((t) => t.id == id);
  if (index !== -1) {
    todos.splice(index, 1);
    res.json({ message: "Todo が削除されました" });
  } else {
    res.status(404).send({ message: "Todo が見つかりません" });
  }
});

// ポート 3000 番でサーバーをリッスンします
app.listen(3000, () => {
  console.log(`サーバーがポート3000でリッスン中です`);
});

解説

// モジュールをインポートします
const express = require("express");
const app = express();
const path = require('path');

上記のコードではExpressというモジュールとpathというモジュールをインポートしています。express()関数を呼び出して作成されたappオブジェクトは、HTTPリクエストを処理するためのメソッドを含みます。

// HTMLファイルをホストするディレクトリを指定します。
app.use(express.static(path.join(__dirname, 'public')));

ここでは、静的なファイル(HTML、CSS、JavaScript、画像ファイルなど)を配信するためのミドルウェアを設定しています。このミドルウェアは、指定されたディレクトリ内のファイルをHTTP経由で直接提供します。

app.use(express.json());

これはミドルウェアの一部で、これを設定することで、サーバーはJSON形式のボディを持つPOSTリクエストを解析できます。

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

このコードでは、ルートパス(/)へのGETリクエストに対する応答を定義しています。応答として、サーバーはpublicディレクトリ内のindex.htmlファイルを返します。

app.get("/todos", (req, res) => {
  res.json({ todos });
});

この部分では、/todosパスに対するGETリクエストの処理を設定しています。これにより、このパスへのリクエストがあったときにはtodos配列をJSON形式で返します。

app.post("/todos", (req, res) => {
  const newTodo = req.body;
  newTodo.id = todos.length + 1;
  todos.push(newTodo);
  res.json({ todo: newTodo });
});

この部分では、/todosパスに対するPOSTリクエストを処理しています。新しいTODO項目を作成し、それをtodos配列に追加します。そして、新しいTODO項目をクライアントにJSON形式で返します。

app.put("/todos/:id", (req, res) => {
  const id = req.params.id;
  let todo = todos.find((t) => t.id == id);
  if (todo) {
    Object.assign(todo, req.body);
    res.json({ todo });
  } else {
    res.status

(404).send({ message: "Todo が見つかりません" });
  }
});

この部分では、/todos/:idパスに対するPUTリクエストを処理しています。指定されたIDを持つTODO項目を探し、それをリクエストのボディで更新します。そして、更新されたTODO項目をクライアントにJSON形式で返します。

app.delete("/todos/:id", (req, res) => {
  const id = req.params.id;
  const index = todos.findIndex((t) => t.id == id);
  if (index !== -1) {
    todos.splice(index, 1);
    res.json({ message: "Todo が削除されました" });
  } else {
    res.status(404).send({ message: "Todo が見つかりません" });
  }
});

この部分では、/todos/:idパスに対するDELETEリクエストを処理しています。指定されたIDを持つTODO項目を探し、それをtodos配列から削除します。そして、クライアントに対して削除が成功したことを示すメッセージをJSON形式で返します。

// ポート 3000 番でサーバーをリッスンします
app.listen(3000, () => {
  console.log(`サーバーがポート3000でリッスン中です`);
});

最後に、サーバーはポート3000でリッスンします。これにより、サーバーはポート3000で受信した全てのHTTPリクエストを処理します。

TODOアプリのフロントエンドを作成する方法

次に、フロントエンド部分を作成します。以下のHTMLとJavaScriptをindex.htmlという名前のファイルに保存します。

<!DOCTYPE html>
<html>
  <head>
    <title>TODOアプリ</title>
    <style>
      /* ここにCSSを書く */
    </style>
  </head>
  <body>
    <h1>TODOアプリ</h1>
    <input id="todo-input" type="text" placeholder="新しいTODOを入力" />
    <button id="add-todo">追加</button>
    <ul id="todo-list"></ul>
    <template id="todo-item-template">
      <li>
        <span class="todo-text"></span>
        <button class="delete-todo">削除</button>
        <button class="update-todo">完了</button>
      </li>
    </template>
    <script>
      const todoInput = document.querySelector("#todo-input");
      const addTodoButton = document.querySelector("#add-todo");
      const todoList = document.querySelector("#todo-list");
      const todoItemTemplate = document.querySelector("#todo-item-template");

      // すべてのTODOを取得し、画面に表示する
      function fetchTodos() {
        fetch("http://localhost:3000/todos")
          .then((response) => response.json())
          .then((data) => {
            todoList.innerHTML = "";
            data.todos.forEach((todo) => {
              const todoItem = todoItemTemplate.content.cloneNode(true);
              const textElement = todoItem.querySelector(".todo-text");
              const deleteButton = todoItem.querySelector(".delete-todo");
              const updateButton = todoItem.querySelector(".update-todo");

              textElement.textContent = todo.text;
              updateButton.textContent = todo.completed ? "未完了" : "完了";

              deleteButton.addEventListener("click", (event) => {
                event.stopPropagation();
                deleteTodo(todo);
              });

              updateButton.addEventListener("click", (event) => {
                event.stopPropagation();
                updateTodo(todo);
              });

              todoList.appendChild(todoItem);
            });
          });
      }

      // 新しいTODOを作成する
      function addTodo() {
        const newTodo = { text: todoInput.value, completed: false };
        fetch("http://localhost:3000/todos", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(newTodo),
        }).then(() => {
          fetchTodos();
          todoInput.value = "";
        });
      }

      // TODOを更新する
      function updateTodo(todo) {
        todo.completed = !todo.completed;
        fetch(`http://localhost:3000/todos/${todo.id}`, {
          method: "PUT",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(todo),
        }).then(fetchTodos);
      }

      // TODOを削除する
      function deleteTodo(todo) {
        fetch(`http://localhost:3000/todos/${todo.id}`, {
          method: "DELETE",
        }).then(fetchTodos);
      }

      addTodoButton.addEventListener("click", addTodo);
      fetchTodos();
    </script>
  </body>
</html>

解説

HTML部分には <template> タグを使用して、TODOアイテムのレイアウトを定義します。

これは非表示の要素で、その内容をJavaScriptで複製して表示するために使用します。

<template id="todo-item-template">
  <li>
    <span class="todo-text"></span>
    <button class="delete-todo">削除</button>
    <button class="update-todo">完了</button>
  </li>
</template>

このテンプレート内の要素は、JavaScriptから参照できるようにクラス名を割り当てています。

const todoItem = todoItemTemplate.content.cloneNode(true);
const textElement = todoItem.querySelector(".todo-text");
const deleteButton = todoItem.querySelector(".delete-todo");
const updateButton = todoItem.querySelector(".update-todo");

新たなTODOアイテムを生成するために、テンプレートの内容を複製し(.content.cloneNode(true))、テキストとボタンをカスタマイズします。ボタンそれぞれには、対応する操作(削除や更新)を行うイベントリスナーを追加しています。

終わりに

この記事では、Express.jsを使用してバックエンドのAPIを作成し、そのAPIを使用してHTMLとJavaScriptでフロントエンドのTODOアプリを作成する方法を紹介しました。ここでは非常に基本的なアプリケーションを作成しましたが、これを基にしてさらに複雑なアプリケーションを作成することも可能です。