Node.jsでTODOアプリケーションの作成方法

概要

Node.js v19.7.0

本記事では、最初にNode.jsを使用してAPIを構築し、その後HTMLとJavaScriptを使用してフロントエンドを作成することで、完全なTODOアプリケーションを作成します。

また、今回作成するコードは全て、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 http = require("http");
const url = require("url");
const qs = require("querystring");

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

// リクエストハンドラを設定
const requestHandler = (req, res) => {
  const parsedUrl = url.parse(req.url);
  const parsedQuery = qs.parse(parsedUrl.query);
  const method = req.method;

  if (method === "GET") {
    if (parsedUrl.pathname === "/") {
      fs.readFile("index.html", (err, data) => {
        if (err) {
          res.writeHead(404);
          res.end(JSON.stringify(err));
          return;
        }
        res.writeHead(200, { "Content-Type": "text/html" });
        res.end(data);
      });
    } else if (parsedUrl.pathname === "/todos") {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify(todos));
    }
  } else if (method === "POST") {
    if (parsedUrl.pathname === "/todos") {
      let body = "";
      req.on("data", (chunk) => {
        body += chunk.toString();
      });
      req.on("end", () => {
        const newTodo = JSON.parse(body);
        newTodo.id = todos.length + 1;
        todos.push(newTodo);
        res.writeHead(201, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ todo: newTodo }));
      });
    }
  } else if (method === "PUT") {
    const regex = /\/todos\/([0-9]+)/;
    const match = regex.exec(parsedUrl.pathname);
    if (match) {
      const id = match[1];
      let body = "";
      req.on("data", (chunk) => {
        body += chunk.toString();
      });
      req.on("end", () => {
        let todo = todos.find((t) => t.id == id);
        if (todo) {
          Object.assign(todo, JSON.parse(body));
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ todo }));
        } else {
          res.writeHead(404, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ message: "Todo が見つかりません" }));
        }
      });
    }
  } else if (method === "DELETE") {
    const regex = /\/todos\/([0-9]+)/;
    const match = regex.exec(parsedUrl.pathname);
    if (match) {
      const id = match[1];
      const index = todos.findIndex((t) => t.id == id);
      if (index !== -1) {
        todos.splice(index, 1);
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ message: "Todo が削除されました" }));
      } else {
        res.writeHead(404, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ message: "Todo が見つかりません" }));
      }
    }
  }
};

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

解説

モジュールのインポート

const http = require("http");
const url = require("url");
const qs = require("querystring");

ここではNode.jsが提供する「http」、「url」、「querystring」の3つのモジュールをインポートしています。これらはそれぞれHTTPサーバーの作成、URLの解析、クエリストリングの解析に使用されます。

初期のTodoリストの作成

let todos = [
  { id: 1, text: "野球をする", completed: false },
  { id: 2, text: "ゲームをする", completed: false },
  { id: 3, text: "仕事をする", completed: false },
];

初期状態のTodoリストを作成しています。各Todoはidtextcompletedの3つのプロパティを持っています。idはTodoの識別子、textはTodoの内容、completedはTodoが完了したかどうかを表します。

リクエストハンドラの作成

const requestHandler = (req, res) => {
  const parsedUrl = url.parse(req.url);
  const parsedQuery = qs.parse(parsedUrl.query);
  const method = req.method;
  ...
};

この部分では、すべてのHTTPリクエストを処理する関数であるリクエストハンドラを作成しています。この関数では、まずリクエストのURLとクエリストリングを解析し、それからHTTPメソッド(GET、POST、PUT、DELETEなど)を取得します。

HTTPメソッドに基づいたリクエストの処理

if (method === "GET") {
    ...
} else if (method === "POST") {
    ...
} else if (method === "PUT") {
    ...
} else if (method === "DELETE") {
    ...
}

この部分では、リクエストのHTTPメソッドに基づいて処理を行っています。GETメソッドではTodoリストを返し、POSTメソッドでは新しいTodoを作成し、PUTメソッドでは既存のTodoを更新し、DELETEメソッドでは既存のTodoを削除します。

HTTPサーバーの作成と開始

const server = http.createServer(requestHandler);
server.listen(3000, () => {
  console.log(`サーバーがポート3000でリッスン中です`);
});

ここでは、先に作成したリクエストハンドラを使ってHTTPサーバーを作成し、ポート3000でサーバーを開始しています。サーバーが開始されると、「サーバーがポート3000でリッスン中です」というメッセージが表示されます。

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((todos) => {
            todoList.innerHTML = "";
            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))、テキストとボタンをカスタマイズします。ボタンそれぞれには、対応する操作(削除や更新)を行うイベントリスナーを追加しています。

終わりに

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