Creating a Complete TODO Application: Building Front and Back Ends with Fastify and HTML

overview

Fastify v4.0.0
nodejs v19.7.0

The first part uses Fastify to build the API, and the second part uses HTML and JavaScript to create the front end, creating a complete TODO application.

In addition, all the code created this time is posted on GitHub.

  1. API endpoints:

  2. GET /todos Get all TODOs

  3. POST /todos create a new TODO

  4. PUT /todos/:id to update an existing TODO

  5. DELETE /todos/:id Delete an existing TODO

  6. Front-end features:

  7. Display the TODO list

  8. Create a new TODO

  9. Update existing TODO

  10. Remove existing TODOs

How to create a TODO app backend

Install required modules

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

server.js

// import the module
const fastify = require("fastify")({ logger: true });
const path = require("path");

// enable CORS settings
fastify.register(require("@fastify/cors"), {
  origin: true, // allow access from all origins
});

// Specify the directory that hosts the HTML files.
fastify.register(require("@fastify/static"), {
  root: path.join(__dirname, "views"),
});

// show view/index.html once the route is accessed
fastify.get("/", (req, reply) => {
  reply.sendFile("index.html");
});

// create a todo for the list
let todos = [
  { id: 1, text: "Play baseball", completed: false },
  { id: 2, text: "Play a game", completed: false },
  { id: 3, text: "work", completed: false },
];

// get list of todos with '/todos' path
fastify.get("/todos", (req, reply) => {
  reply.send({ todos });
});

// Create a new Todo by sending a POST request to the "/todos" path
fastify.post("/todos", (req, reply) => {
  const newTodo = req.body;
  newTodo.id = todos.length + 1;
  push(newTodo);
  reply.send({ todo: newTodo });
});

// update a todo by sending a PUT request to the "/todos/:id" path
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 not found" });
  }
});

// delete a todo by sending a DELETE request to the "/todos/:id" path
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 deleted" });
  } else {
    reply.code(404).send({ message: "Todo not found" });
  }
});

// listen to the server on port 3000
fastify.listen({ port: 3000 }, (err, address) => {
  if (err) throw err;
  fastify.log.info(`server is listening on ${address}`);
});

Commentary

// import the module
const fastify = require('fastify')({ logger: true });
const path = require('path');

The code above imports a module named Fastify and a module named path. A Fastify instance contains methods for handling HTTP requests.

// enable CORS settings
fastify.register(require("@fastify/cors"), {
  origin: true, // allow access from all origins
});

Here we have CORS (Cross-Origin Resource Sharing) enabled. CORS is a mechanism for allowing requests from different origins. Here we allow requests from all origins.

// Specify the directory that hosts the HTML files.
fastify.register(require("@fastify/static"), {
  root: path.join(__dirname, "public"),
});

Here, we set middleware for serving static files (HTML, CSS, JavaScript, image files, etc.). This middleware serves files in the specified directory directly via HTTP.

// show view/index.html once the route is accessed
fastify.get("/", (req, reply) => {
  reply.sendFile("index.html");
});

Here, it is set to display index.html when accessing the root. index.html is the frontend HTML file.

// listen to the server on port 3000
fastify.listen({ port: 3000 }, (err, address) => {
  if (err) throw err;
  fastify.log.info(`server is listening on ${address}`);
});

Finally, the server will listen on port 3000. This will cause the server to handle all HTTP requests received on port 3000.

How to create a frontend for a TODO app

Next, create the front end part. Save the HTML and JavaScript below in a file named index.html. index.html

<!DOCTYPE html>
<html>
  <head>
    <title>TODO App</title>
    <style>
      /* Write your CSS here */
    </style>
  </head>
  <body>
    <h1>TODO App</h1>
    <input id="todo-input" type="text" placeholder="Enter new TODO" />
    <button id="add-todo">Add</button>
    <ul id="todo-list"></ul>
    <template id="todo-item-template">
      <li>
        <span class="todo-text"></span>
        <button class="delete-todo">Delete</button>
        <button class="update-todo">Done</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");

      // Get all TODOs and display them on the screen
      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 ? "not completed" : "completed";

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

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

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

      // create a new 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 = "";
        });
      }

      // update 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);
      }

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

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

Commentary

The HTML part uses <template> tags to define the layout of the TODO items.

This is a hidden element that you use to clone and display its contents with JavaScript.

<template id="todo-item-template">
  <li>
    <span class="todo-text"></span>
    <button class="delete-todo">Delete</button>
    <button class="update-todo">Done</button>
  </li>
</template>

Elements in this template are assigned class names so that they can be referenced from 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");

To generate a new TODO item, clone the content of the template (.content.cloneNode(true)) and customize the text and buttons. Each button adds an event listener that performs the corresponding operation (delete or update).

summary

In this article, I showed you how to create a backend API using Express.js and then use it to create a frontend TODO app in HTML and JavaScript. We’ve built a very basic application here, but you can build on it to build a more complex application.