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/[email protected]"></script> <script src="https://unpkg.com/[email protected]/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/[email protected]/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


Discover more from Coursity

Subscribe to get the latest posts sent to your email.

Leave a Comment

Your email address will not be published. Required fields are marked *

Discover more from Coursity

Subscribe now to keep reading and get access to the full archive.

Continue reading

Scroll to Top