DEV Community

Cover image for Creating a CRUD Application With Express and HTMX
Ethan
Ethan

Posted on • Originally published at ethan-dev.com

Creating a CRUD Application With Express and HTMX

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


Enter fullscreen mode Exit fullscreen mode

Next install the packages required for this project:



yarn add express body-parser cors


Enter fullscreen mode Exit fullscreen mode

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



mkdir src


Enter fullscreen mode Exit fullscreen mode

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');


Enter fullscreen mode Exit fullscreen mode

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/')));


Enter fullscreen mode Exit fullscreen mode

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' }
];


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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/🆔 Updates an existing todo based on the provided ID
  • DELETE /api/todos/🆔 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}`);
});


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>



Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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

Top comments (8)

Collapse
 
kasir-barati profile image
Mohammad Jawad (Kasir) Barati

This more work than I expected. Not to mention that you are returning good old JSON. I watched some YouTube videos and they were returning HTML from their backend. I was kinda looking for something like this here.

BTW I am not trying to say that this post was for nothing, rather I would say it opened my eye in terms of I was thinking that in htmx you can only return HTML from your backend. But now I know the answer is definitely no. In fact I am kinda relieved. My backend app still can work with htmx.

Collapse
 
chasm profile image
Charles F. Munat • Edited

What's with the <h5> elements? I didn't see any <h2>, <h3>, or <h4> elements.

When your HTML is wrong, it damages the credibility of everything else. I stopped reading when I saw that.

Headings

Collapse
 
poetro profile image
Peter Galiba • Edited

Generating the id based on the length of the array is wrong. Let's say, I delete the id = 0 element, then insert on new. Now I have two items with the same id .

Collapse
 
thomas_hunkin_d5aff377e3d profile image
Thomas Hunkin
Collapse
 
kasir-barati profile image
Mohammad Jawad (Kasir) Barati

Loved the essay. It was supper funny. But I am still not sure if he is trying to really bad mouth his own framework or just being sarcastic.

Collapse
 
tanzimibthesam profile image
Tanzim Ibthesam • Edited

This is not htmx maybe just used 1 or two features. You would not send http requests through fetch ajax or axios instead through templating engines or html file. Look at the documentation or traversy medias YT tutorial

Collapse
 
drtobal profile image
Cristóbal Díaz Álvarez

Exactly

Collapse
 
tanzimibthesam profile image
Tanzim Ibthesam

Can you address this issue? Is this really an easy way or even HTMX