The reason behind me, embarking on another educational journey is simple. I applied for a job and one of its requirements was htmx. So I thought lets look at this new alien technology and to my surprise I did enjoy tinkering around with it.
My final thoughts first
It does not have as much as learning curve + tooling around it, compare to things like ReactJS, and other frontend frameworks.
It is a novel experience to say the least for me. I mean yes I have been using Handlebars, pug, and other templating engines but this is novel in how it changed my perspective about HTML (Just read their motivation in htmx.org).
Testing is a bit confusing for me but I can guess we might use tools like Cypress.
Having your backend returning HTML needs further research to be able to discuss and make rational decisions regarding how you're gonna serve other clients of your backend (mobile, IoT, other backend apps, etc).
You have to address security concerns (XSS).
You have to think about the way that they are using strings all over the place, no type safety, smallest typo can lead to hours of debugging, and lastly how can you make modular web apps with htmx.
Bear with me, it is not really my forte to take the easy way out. But right now I wanted to just get a feeling of this tech. So you might as well try this. Then you can decide whether it worth the money to learn it or not.
We're gonna use
Steps
I split it into two part. Backend and frontend.
Backend
1.mkdir todo && cd todo. 2.mkdir backend frontend && cd backend. 3.pnpm init && pnpm add "express@>=5.0.0" cors --save. 4. Add "type": "module" to your package.json, and this script: "dev": "node src/index.js" 5.mkdir src && touch src/index.js.
Write the following in it:
// @ts-checkimportexpressfrom"express";importcorsfrom"cors";import{buildTodosList}from"./build-todos-list.js";import{todoRepo}from"./todos.js";constapp=express();app.use(cors());app.use(express.json());// To be able to receive form data send by our form. We will create it in frontend part of this post.app.use(express.urlencoded());app.post("/todos",(req,res)=>{/**@type {{newTodo: string }} */const{newTodo}=req.body;consttodos=todoRepo.create(newTodo);res.set("content-type","text/html");res.status(201).send(buildTodosList(todos));});app.get("/todos",(req,res)=>{consttodos=todoRepo.read();res.set("content-type","text/html");res.status(200).send(buildTodosList(todos));});app.put("/todos/:id",(req,res)=>{constid=req.params.id;consttodos=todoRepo.update(id);res.set("content-type","text/html");res.status(200).send(buildTodosList(todos));});app.delete("/todos/:id",(req,res)=>{constid=req.params.id;consttodos=todoRepo.delete(id);res.set("content-type","text/html");res.status(200).send(buildTodosList(todos));});app.listen(3000,"localhost",console.log.bind(this,"Server is up and running on port 3000"));
6.touch src/build-todos-list.js.
And inside it write this code:
// @ts-check/**
*
* @param {Array<{id: string, title: string, completed: boolean}>} todos
* @returns {string}
*/exportfunctionbuildTodosList(todos){// Just to prevent reordering after an update. You can comment this line and use the todos directly to see the effect. In a real world app this should not happen since the sorting criteria gonna stay the same between requests unless user change it.constsortedTodos=todos.sort((a,b)=>a.title.localeCompare(b.title));returnsortedTodos.reduce((accumulator,todo)=>{accumulator+=`
<li>
<input
type="checkbox"
id="todo_${todo.id}"
${todo.completed?"checked":""}
hx-put="/todos/${todo.id}"
hx-trigger="click"
hx-target="#todo-list"
/>
<label for="todo_${todo.id}">${todo.title}</label>
<button
hx-delete="/todos/${todo.id}"
hx-trigger="click"
hx-target="#todo-list"
>X</button>
</li>
`;returnaccumulator;},"");}
7.touch src/todos.js and then write this simple code in it:
// @ts-checkimport{randomUUID}from"crypto";exportconsttodoRepo={_data:[{id:randomUUID(),title:"Finish AI project",completed:false,},{id:randomUUID(),title:"Review JavaScript notes",completed:false,},{id:randomUUID(),title:"Buy groceries",completed:false,},{id:randomUUID(),title:"Run for 30 min",completed:true,},{id:randomUUID(),title:"Read AI research papers",completed:false,},{id:randomUUID(),title:"Plan weekend trip",completed:false,},{id:randomUUID(),title:"Exercise for 30 minutes",completed:true,},{id:randomUUID(),title:"Write blog post",completed:false,},{id:randomUUID(),title:"Organize desk",completed:true,},{id:randomUUID(),title:"Schedule doctor appointment",completed:false,},],/**@param {string} newTodo */create(newTodo){this._data=[...this._data,{id:randomUUID(),title:newTodo,completed:false,},];returnthis._data;},read(){returnthis._data;},/**@param {string} id */update(id){constotherTodos=this._data.filter((todo)=>todo.id!==id);consttodo=this._data.filter((todo)=>todo.id===id)[0];this._data=[...otherTodos,{...todo,completed:!todo.completed,},];returnthis._data;},/**@param {string} id */delete(id){this._data=this._data.filter((todo)=>todo.id!==id);returnthis._data;},};
8. And then you should be able to launch your backend app with pnpm dev command.
Frontend
First go back to the root of this project, you're terminal should be in todo/ and not todo/backend.
1.cd /frontend && touch index.html 2. Go to the htmx.org and download it. Renamed the downloaded file to htmx.2.0.3.min.js and placed it next to index.html. 3. Write the following markup in your index.html
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><title>Todo</title><script src="./htmx.2.0.3.min.js"></script><linkhref="./index.css"rel="stylesheet"/><basehref="http://localhost:3000"/></head><body><headerclass="header"><h1>Todo app</h1><selectid="theme"><optionvalue="dark"selected>Dark</option><optionvalue="light">Light</option></select></header><mainclass="main"><sectionclass="add-todo"><h2>Add new todo</h2><formclass="add-todo__form"hx-post="/todos"hx-target="#todo-list"><p><labelfor="newTodo">Todo</label><inputtype="text"name="newTodo"id="newTodo"/></p><buttontype="submit">Add</button></form></section><sectionclass="todos"><h2>Todos</h2><ulhx-get="/todos"hx-trigger="load"id="todo-list"></ul></section></main></body></html>
NOTE
I am not gonna talk about CSS here. So I just let you go and read it in my repo for this post (link down below).
Now with this done you are all set. Just open this index.html in your browser and play around with your todo app.
Important
You need to use the base element in your HTML document. Or <meta name="htmx-config" content='{"selfRequestsOnly": false}' /> to be able to send http requests with hx-get and other hx-* attributes to external URL addresses. If you do not add it it will throw this error message at you which does not really provide much of context and help to debug it:
A simple Todo app which utilizes htmx + ExpressJS as its backend
Todo
A simple Fullstack app written in ExpressJS + htmx. It is really fun to work with htmx. I also wrote a dev.to post about it. You can read it here.
Note
But it also has its own downside. Like when your backend should serve other clients other than htmx, then what? because as you can see in my code I am returning HTML as response. But nonetheless it is a very intriguing approach to developing web applications.
How to run it
cd backend.
pnpm install.
pnpm dev.
Open frontend/index.html in your browser.
Now you should be good to go, try to remove, add or check some of the todos ;).