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
- nodeJS installed (https://nodejs.org/)
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">×</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">×</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.
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