Creating a CRUD Application With Express and HTMX

Creating a CRUD Application With Express and HTMX

ยท

7 min read


Introduction

Hello! ๐Ÿ˜Ž

In this tutorial I will show you how to create a simple Todo CRUD application using Express for the backend and HTMX for the frontend.

Creating a CRUD(Create, Read, Update, Delete) application is a great way to understand the basics of web development. By the end of this tutorial, you'll have a working application that allows you to add, view, edit and delete tasks. Let's get coding! ๐Ÿ˜ธ


Requirements


Setting Up the Backend With Express

First we need an API server, so to keep things simple I will be using Express.

First create a new directory for the project and initialize it:

mkdir htmx-crud && cd htmx-crud
yarn init -y

Next install the packages required for this project:

yarn add express body-parser cors

Now we need to create a src folder to store the source code files:

mkdir src

Create a new file in the newly created src directory called "server.js", first we will import the required modules:

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const path = require('path');

Next we need to initialize express and load the required middleware, this can be done via the following:

const app = express();
const PORT = 3000;

app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, '../public/')));

The above initializes express and loads the required middleware, we will be handling JSON and we have enabled cors for all origins.

Next we will define the todo list array with mock data:

let todos = [
  { id: 1, task: 'Learn HTMX' },
  { id: 2, task: 'Feed Cat' }
];

Now to define the routes that will be needed by the front end, here is the routes for all CRUD operations:

app.get('/api/todos', (req, res) => {
  try {
    res.status(200).json(todos);
  } catch (error) {
    console.error('Failed to get todos', error);
  }
});

app.post('/api/todos', (req, res) => {
  try {
    const newTodo = { id: todos.length + 1, task: req.body.task };
    todos.push(newTodo);

    res.status(201).json(newTodo);
  } catch (error) {
    console.error('Failed to create todo', error);
  }
});

app.put('/api/todos/:id', (req, res) => {
  try {
    const id = parseInt(req.params.id);
    const todo = todos.find(t => t.id === id);

    if (!todo) {
      res.status(404).send('Todo not found');

      return;
    }

    todo.task = req.body.task;

    res.status(200).json(todo);
  } catch (error) {
    console.error('failed to edit todo', error);
  }
});

app.delete('/api/todos/:id', (req, res) => {
  try {
    const id = parseInt(req.params.id);
    todos = todos.filter(t => t.id !== id);

    res.status(204).send();
  } catch (error) {
    console.error('failed to delete todo', error);
  }
});

The above defines four routes:

  • GET /api/todos: This route returns the list of todos

  • POST /api/todos: This route adds a new todo to the list

  • PUT /api/todos/:id: Updates an existing todo based on the provided ID

  • DELETE /api/todos/:id: Deletes a todo based on the provided ID

Finally we will end the server side by providing an index route to server the HTML file, this is done via the following code:

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

app.listen(PORT, () => {
  console.log(`server is running on port ${PORT}`);
});

Phew! Thats the server finished, now we can start coding the frontend! ๐Ÿฅธ


Setting Up the Frontend with HTMX

First create a directory called "public":

mkdir public

Create a new file in the public directory called "index.html" and add the following head tag:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HTMX CRUD</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://unpkg.com/htmx.org@1.6.1"></script>
    <script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/client-side-templates.js"></script>
  </head>

We will be using HTMX and the styling will be done via Bootstrap. Make sure to add the closing tags for each tag.

First we will create a container and create the modal and form that will be used to create a new todo item:

  <body>
    <div class="container">
      <h1 class="mt-5">Sample HTMX CRUD Application</h1>
      <div id="todo-list" hx-get="/api/todos" hx-trigger="load" hx-target="#todo-list" hx-swap="innerHTML" class="mt-3"></div>
      <button class="btn btn-primary mt-3" data-toggle="modal" data-target="#addTodoModal">Add Todo</button>
    </div>

    <!-- Add Todo Modal -->
    <div class="modal fade" id="addTodoModal" tabindex="-1" role="dialog" aria-labelledby=addTodoModalLabel" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="addTodoModalLabel">Add Todo</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>

          <div class="modal-body">
            <form hx-post="/api/todos" hx-target="#new-todo-container" hx-swap="beforeend">
              <div class="form-group">
                <label for="task">Task</label>
                <input type="text" class="form-control" id="task" name="task" required />
              </div>
              <button type="submit" class="btn btn-primary">Add</button>
            </form>
          </div>
        </div>
      </div>
    </div>

    <div id="new-todo-container" style="display: none;"></div>

In the above we define a modal that contains a form for adding new todo items to the list.

The form uses HTMX attributes "hx-post" to specify the URL for adding todos, "hx-target" to specify where to inset the new todo, and "hx-swap" to determine how the response is handled.

Next we will add the modal for editing todos:

    <!-- Edit Todo Modal -->
    <div class="modal fade" id="editTodoModal" tabindex="-1" role="dialog" aria-labelledby="editTodoModalLabel" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="editTodoModalLabel">Edit Todo</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <form id="editTodoForm">
              <div class="form-group">
                <label for="editTask">Task</label>
                <input type="text" class="form-control" id="editTask" name="task" required />
              </div>
              <button type="submit" class="btn btn-primary">Save</button>
            </form>
          </div>
        </div>
      </div>
    </div>

The above modal is similiar to the add modal but will be used for editing existing todos. Note this time it does not contain HTMX attributes because we will handle the form submission with JavaScript.

Next we will use a HTMX template to display the todos in a Bootstrap card:

    <!-- Todo Template -->
    <script type="text/template" id="todo-template">
      <div class="card mb-2" id="todo-{{id}}">
        <div class="card-body">
          <h5 class="card-title item-task">{{task}}</h5>
          <button class="btn btn-warning" onclick="openEditModal('{{id}}', '{{task}}')">Edit</button>
          <button class="btn btn-danger" onclick="deleteTodo('{{id}}')">Delete</button>
        </div>
      </div>
    </script>

In the above script we define a HTML template for displaying each todo item. The template uses placeholders that are in braces, this will be replaced with actual data.

Finally add the JavaScript to handle various functions:

    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

    <script>
      function renderTodoItem(todo) {
        const template = document.getElementById('todo-template').innerHTML;

        return template.replace(/{{id}}/g, todo.id).replace(/{{task}}/g, todo.task);
      }

      function openEditModal(id, task) {
        const editForm = document.getElementById('editTodoForm');
        editForm.setAttribute('data-id', id);
        document.getElementById('editTask').value = task;
        $('#editTodoModal').modal('show');
      }

      function deleteTodo(id) {
        fetch(`/api/todos/${id}`, {
          method: 'DELETE'
        })
        .then(() => {
          document.querySelector(`#todo-${id}`).remove();
        });
      }

      document.addEventListener('htmx:afterRequest', (event) => {
        if (event.detail.requestConfig.verb === 'post') {
          document.querySelector('#addTodoModal form').reset();
          $('#addTodoModal').modal('hide');

          const newTodo = JSON.parse(event.detail.xhr.responseText);
          const todoHtml = renderTodoItem(newTodo);

          document.getElementById('todo-list').insertAdjacentHTML('beforeend', todoHtml);

          event.preventDefault();
        } else if (event.detail.requestConfig.verb === 'put') {
          $('#editTodoModal').modal('hide');
        }
      });

      document.addEventListener('htmx:afterSwap', (event) => {
        if (event.target.id === 'todo-list') {
          const todos = JSON.parse(event.detail.xhr.responseText);

          if (Array.isArray(todos)) {
            let html = '';

            todos.forEach(todo => {
              html += renderTodoItem(todo);
            });

            event.target.innerHTML = html;
          } else {
            const todoHtml = renderTodoItem(todos);
            event.target.insertAdjacentHTML('beforeend', todoHtml);
          }
        }
      });

      document.getElementById('editTodoForm').addEventListener('submit', function (event) {
        event.preventDefault();

        const id = event.target.getAttribute('data-id');
        const task = document.getElementById('editTask').value;

        fetch(`/api/todos/${id}`, {
          method: 'PUT',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ task })
        })
        .then(response => response.json())
        .then(data => {
          const todoHtml = renderTodoItem(data);
          document.querySelector(`#todo-${id}`).outerHTML = todoHtml;
          $('#editTodoModal').modal('hide');
        })
        .catch(error => console.error(error));
      });
    </script>
  </body>
</html>

In the above:

  • renderTodoItem(todo): Renders a todo item using the previously defined template

  • openEditModal(id, task): Opens the modal to edit the todo

  • deleteTodo(id): Deletes a todo item

  • Event listeners handle after-request and after-swap events for HTMX to manage the modal states and update the DOM.

Done! Next we can finally run the server! ๐Ÿ˜†


Running the Application

To run the application, open your terminal and navigate to the project directory. Start the server with the following command:

node src/server.js

Open your browser and navigate to "http://localhost:3000". You should see your CRUD application running. You can add, edit and delete tasks, and the changes will be reflected without reloading the page. ๐Ÿ‘€


Conclusion

In this tutorial I have shown you how to build a simple CRUD application using Express and HTMX. This application allows you to add, view, edit and delete tasks without the need for any page reloading. We've used Bootstrap for styling and HTMX for handling AJAX requests. By following this tutorial, you should now have a good understanding of how to build a CRUD application with Express and HTMX.

Feel free to try implement a database to store the todos and improve on this example!

As always you can find the code on my Github: https://github.com/ethand91/htmx-crud

Happy Coding! ๐Ÿ˜Ž


Like my work? I post about a variety of topics, if you would like to see more please like and follow me. Also I love coffee.

โ€œBuy Me A Coffeeโ€

If you are looking to learn Algorithm Patterns to ace the coding interview I recommend the [following course](https://algolab.so/p/algorithms-and-data-structure-video-course?affcode=1413380_bzrepgch

Did you find this article valuable?

Support Development Diary by becoming a sponsor. Any amount is appreciated!

ย