完全なTODOアプリケーションの作成: FastifyとHTMLを使用したフロントエンドとバックエンドの構築

概要

Fastify v4.0.0
nodejs v19.7.0

前半ではFastifyを使用して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アプリのバックエンドを作成する方法

必要モジュールのインストール

npm install fastify
npm install @fastify/static
npm install @fastify/cors

server.js

// モジュールをインポートします
const fastify = require("fastify")({ logger: true });
const path = require("path");

// CORS設定を有効化します
fastify.register(require("@fastify/cors"), {
  origin: true, // すべてのオリジンからのアクセスを許可します
});

// HTMLファイルをホストするディレクトリを指定します。
fastify.register(require("@fastify/static"), {
  root: path.join(__dirname, "views"),
});

// ルートにアクセスしたら、view/index.htmlを表示します
fastify.get("/", (req, reply) => {
  reply.sendFile("index.html");
});

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

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

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

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

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

// ポート 3000 番でサーバーをリッスンします
fastify.listen({ port: 3000 }, (err, address) => {
  if (err) throw err;
  fastify.log.info(`サーバーが ${address} でリッスン中です`);
});

解説

// モジュールをインポートします
const fastify = require('fastify')({ logger: true });
const path = require('path');

上記のコードではFastifyというモジュールとpathというモジュールをインポートしています。Fastifyインスタンスは、HTTPリクエストを処理するためのメソッドを含みます。

// CORS設定を有効化します
fastify.register(require("@fastify/cors"), {
  origin: true, // すべてのオリジンからのアクセスを許可します
});

ここでは、CORS(Cross-Origin Resource Sharing)を有効にしています。CORSは、異なるオリジンからのリクエストを許可するための仕組みです。ここでは、すべてのオリジンからのリクエストを許可しています。

// HTMLファイルをホストするディレクトリを指定します。
fastify.register(require("@fastify/static"), {
  root: path.join(__dirname, "public"),
});

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

// ルートにアクセスしたら、view/index.htmlを表示します
fastify.get("/", (req, reply) => {
  reply.sendFile("index.html");
});

ここでは、ルートにアクセスしたらindex.htmlを表示するように設定しています。index.htmlは、フロントエンド部分のHTMLファイルです。

// ポート 3000 番でサーバーをリッスンします
fastify.listen({ port: 3000 }, (err, address) => {
  if (err) throw err;
  fastify.log.info(`サーバーが ${address} でリッスン中です`);
});

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

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

次に、フロントエンド部分を作成します。以下のHTMLとJavaScriptをindex.htmlという名前のファイルに保存します。 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アプリを作成する方法を紹介しました。ここでは非常に基本的なアプリケーションを作成しましたが、これを基にしてさらに複雑なアプリケーションを作成することも可能です。