
Fatigue Javascript : HTMX est tout ce dont vous avez besoin pour créer ChatGPT — Partie 1
Il fut un temps, il y a bien longtemps, où créer des sites Web était facile. HTML et CSS. C’était simple. De nos jours, les frameworks Javascript sont partout. Changement incessant, complexité croissante. Ce phénomène est appelé « fatigue Javascript » et concerne les développeurs épuisés par la recherche des derniers frameworks, la création d’outils, de bibliothèques et la tentative de suivre le rythme. Avec HTMX, les développeurs disposent désormais d’un moyen de créer des applications Web attrayantes avec plus de simplicité et moins d’épuisement professionnel, et sans tous les tracas de JS.
Une application web engageante comme ChatGPT, en moins de 200 lignes de code, pur Python et HTML. Comme celui-ci :

Un petit rappel sur le fonctionnement du Web
Lorsque Tim Berners-Lee a créé la première page Web en 1990, le système qu’il a conçu était principalement un système « en lecture seule », qui mènerait à des pages reliées entre elles par des hyperliens, que nous appelons tous des balises d’ancrage en HTML. HTML 1.0 reposait donc sur une seule balise et proposait une navigation simple entre les pages.
<!-- The original web: simple hypermedia -->
<a href="/about">About Us</a>
La balise d’ancrage est un contrôle hypermédia qui effectue le processus suivant :
- montrer à l’utilisateur qu’il s’agit d’un lien (cliquable)
- émettre une requête GET à l’URL du lien hypertexte
Lorsque le serveur répond avec une nouvelle page, le navigateur remplacera la page actuelle par la nouvelle page (navigation)
Puis vint le Web 2.0 qui introduisit une nouvelle balise, la balise form. Cette balise permettait de mettre à jour les ressources en plus de les lire via le <a> étiqueter. Pouvoir mettre à jour les ressources nous a permis de réellement commencer à créer des applications Web. Tout cela avec seulement deux contrôles : <form> et <a>.
<!-- Web 2.0: now we can update data -->
<form method="POST" action="/login">
<input type="email" name="email" required>
<input type="password" name="password" required>
<button type="submit">Login</button>
</form>
Le processus de soumission d’un formulaire est assez similaire à la balise d’ancrage, sauf que l’on peut :
- choisir le type de requête que nous voulons effectuer (GET ou POST)
- joindre des informations utilisateur telles que l’e-mail, le mot de passe, etc. à transmettre avec la demande
Les deux balises sont les seuls éléments, en HTML pur, pouvant interagir avec un serveur.
Et puis est arrivé Javascript.
JavaScript a été créé à l’origine pour ajouter des interactions simples aux pages Web : validation de formulaires, récupération de données et animations de base. Mais avec l’introduction de XMLHttpRequest (plus tard connu sous le nom d’AJAX), JavaScript a évolué vers quelque chose de beaucoup plus puissant et complexe.
Avec Javascript, les développeurs peuvent désormais déclencher des requêtes HTTP sans les deux balises, en utilisant quelque chose appelé AJAX. AJAX permet de récupérer des données depuis le serveur, et bien que XHR puisse récupérer tout type de données, y compris des fragments HTML bruts, du texte ou du XML, JSON est devenu le format d’échange de données de facto.
Cela signifie qu’il doit y avoir une étape supplémentaire où JSON est converti en HTML, via une fonction qui restitue le HTML à partir de JSON. Comme le montre l’exemple ci-dessous, nous procédons par :
- récupérer les données JSON à partir du
/api/userspoints finaux (lesresponse => response.json()partie) - en insérant ces données dans un modèle HTML (le
const htmlpartie) - qui sera ensuite ajouté au DOM (le
document.getElementById()partie)
// The JavaScript way: JSON → HTML conversion
fetch('/api/users')
.then(response => response.json())
.then(users => {
const html = users.map(user =>
`<div class="user">${user.name}</div>`
).join('');
document.getElementById('users').innerHTML = html;
});
Ce rendu implique un couplage étroit entre le format de données JSON et la fonction elle-même : si le format de données JSON change, cela pourrait casser la fonction de rendu HTML. Vous voyez déjà un problème potentiel ici, et ce point est généralement un point de friction entre les développeurs frontend et backend : le développeur frontend construit une interface utilisateur basée sur un format JSON attendu, le développeur backend décide de changer le format, le développeur frontend doit mettre à jour l’interface utilisateur, le développeur backend change à nouveau, le développeur frontend change à nouveau, etc.
Pour une raison quelconque, les développeurs Web ont commencé à mettre JSON partout et à tout gérer avec JS. Cela a conduit à ce que nous appelons des applications à page unique (SPA) : contrairement au HTML 2.0 traditionnel, nous ne naviguons plus entre les pages. Tout le contenu reste sur une seule page et le contenu est mis à jour avec le rendu JS et UI. C’est ainsi que fonctionnent des frameworks comme React, Angular, Vue.js.
« La norme émergente pour le développement Web est de créer une application React monopage, avec un rendu serveur. Les deux éléments clés de cette architecture sont quelque chose comme :
– L’interface utilisateur principale est construite et mise à jour en JavaScript à l’aide de React ou quelque chose de similaire.
– Le backend est une API sur laquelle cette application effectue des requêtes.
Cette idée a vraiment balayé Internet. Cela a commencé avec quelques grands sites Web populaires et s’est glissé dans des domaines tels que les sites de marketing et les blogs.(Tom MacWright, https://macwright.com/2020/05/10/spa-fatigue)
La plupart des architectures SPA actuelles sont des applications « client » où la majeure partie du travail se produit côté client et où le backend est simplement une API renvoyant JSON. Cette configuration est connue pour offrir des expériences utilisateur rapides et fluides, mais avons-nous vraiment besoin de cette complexité à chaque fois ?
« (…) il y a aussi beaucoup de problèmes pour lesquels je ne vois aucun avantage concret à utiliser React. Ce sont des choses comme les blogs, les sites Web de paniers d’achat, principalement les sites Web CRUD et de formulaires. «
(Tom MacWright, https://macwright.com/2020/05/10/spa-fatigue)
La fatigue Javascript est réelle
La « fatigue du Javascript » se fait de plus en plus forte. Il fait référence aux principaux inconvénients du développement du SPA :
- Complexité croissante: Les bibliothèques et les frameworks sont devenus de plus en plus lourds et complexes, nécessitant de grandes équipes à gérer. Certains frameworks avisés signifient également que les développeurs JS doivent se spécialiser sur une seule technologie. Aucun développeur Python ne s’est jamais appelé « développeur Tensorflow Python ». Ce ne sont que des développeurs Python, et passer de TF à Pytorch signifie toujours que vous pouvez lire et utiliser les deux.
- Accouplement serré: Le couplage entre les API de données et l’UI crée des frictions au sein des équipes. Des changements radicaux se produisent tous les jours, et il n’y a aucun moyen de résoudre ce problème tant que les équipes utilisent JSON comme interface d’échange.
- Prolifération du cadre: Le nombre de frameworks ne cesse d’augmenter, entraînant un réel sentiment de « fatigue » chez les développeurs JS.
- Sur-ingénierie: Vous n’avez pas besoin de frameworks lourds en JS 90 % du temps. Et dans certains cas (applications riches en contenu), c’est même une mauvaise idée.
À l’exception des interfaces utilisateur hautement interactives/collaboratives, un simple HTML avec des applications multipages suffit souvent.
Alors, qu’est-ce que le HTMX ?
HTMX est une bibliothèque JS très légère (14 Ko) qui offre une approche centrée sur HTML pour créer des applications Web dynamiques. Il étend le HTML en permettant à n’importe quel élément de faire des requêtes AJAX et de mettre à jour n’importe quelle partie du DOM. Contrairement aux frameworks JS qui effectuent tout le rendu côté client, le gros du travail est effectué par le serveur en renvoyant des fragments HTML à insérer dans le DOM. Cela signifie également que si vous connaissez déjà les moteurs de création de modèles et HTML, la courbe d’apprentissage sera beaucoup plus facile que l’apprentissage de React ou Angular.
Au lieu d’abandonner l’hypermédia pour les API JSON, HTMX rend le HTML plus performant avec les éléments suivants :
- N’importe quel élément peut faire des requêtes HTTP (pas seulement
<a>et<form>) - Toute méthode HTTP (GET, POST, PUT, DELETE, PATCH)
- N’importe quel élément peut être ciblé pour les mises à jour
- Tout événement peut déclencher des requêtes (clic, soumission, chargement, etc.)
En fait, vous pouvez réellement écrire votre propre petite interface utilisateur de type GPT avec HTMX et seulement quelques lignes de Python !
Une vraie démo : une application ChatGPT avec HTMX et FastAPI
Pour cet article, nous allons construire un petit chat avec moins de 100 lignes de Python et HTML. Nous commencerons par des démos très simples pour montrer comment fonctionne HTMX, puis ajouterons une interface utilisateur de chat simple, puis ajouterons une fonctionnalité de streaming à notre chat. Pour rendre les choses encore plus attrayantes, nous utiliserons le kit d’outils de développement d’agents Google, afin de pouvoir exploiter les agents dans notre chat !
Démonstrations HTML simples
Supposons que nous ayons une API qui renvoie une liste d’utilisateurs. Nous voulons cliquer sur un bouton pour récupérer les données et afficher une liste.

La méthode JS traditionnelle :
<!-- Traditional JavaScript approach -->
<!DOCTYPE html>
<html>
<head>
<title>Demo</title>
</head>
<body>
<h1>Users</h1>
<button onclick="getUsers()">Show</button>
<div>
<ul id="usersList">
</ul>
</div>
<script>
function getUsers() {
fetch('https://dummyjson.com/users')
.then(res => res.json())
.then(data => {
const usersList = document.getElementById('usersList');
if (usersList) {
data.users.forEach(user => {
const listItem = document.createElement('li');
listItem.textContent = `${user.firstName} ${user.lastName}`;
usersList.appendChild(listItem);
});
}
})
.catch(error => {
console.error('Error fetching users:', error);
});
}
</script>
</body>
</html>
Et c’est comme ça que vous feriez avec HTMX.
Créez d’abord votre backend :
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
import requests
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
return templates.TemplateResponse("demo.html", {"request": request})
@app.get("/users")
async def get_users():
r = requests.get("https://dummyjson.com/users")
data = r.json()
html = ""
for row in data['users']:
html += f"<li>{row['firstName']} {row['lastName']}</li>\n"
return HTMLResponse(html)
Et puis le HTML :
<!-- Traditional JavaScript approach -->
<!DOCTYPE html>
<html>
<head>
<title>Demo</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script>
</head>
<body>
<h1>Users</h1>
<button hx-get="/users" hx-target="#usersList" hx-swap="innerHTML">Show</button>
<div>
<ul id="usersList">
</ul>
</div>
</body>
Et vous obtenez exactement le même résultat ! Que s’est-il passé ici ? Regardez le <button> élément. Nous voyons 3 attributs commençant par hx-. Pourquoi sont-ils là ?
hx-get: Cliquer sur ce bouton déclenchera une requête GET vers le/userspoint finalhx-target: Il indique au navigateur de remplacer le contenu de l’élément qui a leusersListidentifiant avec les données HTML reçues du serveurhx-swap: Il indique au navigateur d’insérer le code HTML à l’intérieur de l’élément cible
Avec cela, vous savez déjà utiliser HTMX. Ce qui est bien avec cette façon de procéder, c’est que si vous décidez de modifier votre code HTML, cela ne cassera rien sur votre page.
Il y a bien sûr des avantages et des inconvénients à utiliser HTMX. Mais en tant que développeur Python, c’est très agréable de jouer avec mon backend FastAPI et de ne pas trop se soucier du rendu HTML. Ajoutez simplement des modèles Jinja, une dose de Tailwind CSS, et vous êtes prêt à partir !
Notre première conversation avec HTMX et FastAPI
C’est donc le moment où les choses deviennent sérieuses. Ce que nous allons faire, dans un premier temps, est de créer un chatbot stupide qui prendra la requête des utilisateurs et la crachera à l’envers. Pour cela nous allons construire une page avec :
- une liste de messages
- une zone de texte pour la saisie de l’utilisateur
Et devinez quoi, HTMX se chargera d’envoyer/recevoir les messages ! Voici à quoi ressemblera le résultat :

Aperçu
Le flux est le suivant :
- L’utilisateur saisit une requête dans une zone de texte
- Cette zone de texte est enveloppée dans un formulaire qui enverra une requête POST au serveur avec le
queryparamètre. - Le backend reçoit la requête, fait quelque chose avec le
query(dans la vraie vie, on peut utiliser un LLM pour répondre à la requête). Dans notre cas, à des fins de démonstration, nous répondrons simplement en inversant la requête lettre par lettre. - Le backend enveloppe la réponse dans une HTMLResponse (pas JSON !)
- Dans notre formulaire, HTMX indique au navigateur où insérer la réponse, comme indiqué dans le
hx-targetet comment l’échanger avec le DOM actuel
Et c’est tout. Alors commençons !
Back-end
Nous définirons un /send itinéraire qui attend un query chaîne du frontend, l’inverse et la renvoie dans un <li> étiqueter.
from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
import asyncio
import time
app = FastAPI()
templates = Jinja2Templates("templates")
@app.get("/")
async def root(request: Request):
return templates.TemplateResponse(request, "simple_chat_sync.html")
@app.post("/send")
async def send_message(request: Request, query: str=Form(...)):
message = "".join(list(query)[::-1])
html = f"<li class='mb-6 justify-end flex'><div class='max-w-[70%] bg-black text-white rounded-xl px-4 py-2'><div class='font-bold text-right'>AI</div><div>{message}</div></div></li>"
return HTMLResponse(html)
L’extrémité avant
Côté frontend, nous définissons une page HTML simple en utilisant Tailwind CSS et HTMX :
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"
integrity="sha384-A986SAtodyH8eg8x8irJnYUk7i9inVQqYigD6qZ9evobksGNIXfeFvDwLSHcp31N"
crossorigin="anonymous"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Merriweather:[email protected]&display=swap" rel="stylesheet">
<style>
body {
font-family: "Merriweather";
}
</style>
</head>
<body class="flex w-full bg-white h-screen">
<main class="flex flex-col w-full md:w-3/4 lg:w-1/2 pb-4 justify-between items-left mx-auto ">
<header class="border-b p-4 text-2xl text-right">
// ZeChat
</header>
<div class="mb-auto max-h-[80%] overflow-auto">
<ul id="chat" class="rounded-2xl p-4 mb-16 justify-start">
</ul>
</div>
<footer class="p-4 border-t">
<form id="userInput" class="flex max-h-16 gap-4" hx-post="/send" hx-swap="beforeend" hx-target="#chat"
hx-trigger="click from:#submitButton" hx-on::before-request="
htmx.find('#chat').innerHTML += `<li class='mb-6 justify-start flex'><div class='max-w-[70%] border border-black rounded-xl px-4 py-2'><div class='font-bold'>Me</div><div>${htmx.find('#query').value}</div></div></li>`;
htmx.find('#query').value = '';
">
<textarea id="query" name="query"
class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 min-h-[44px] max-h-[200px]"
placeholder="Write a message..." rows="4"></textarea>
<button type="submit" id="submitButton"
class="inline-flex max-h-16 items-center justify-center rounded-md bg-neutral-950 px-6 font-medium text-neutral-50 transition active:scale-110">Send</button>
</form>
</footer>
</main>
</body>
</html>
Regardons de plus près le <form> étiqueter. Cette balise a plusieurs attributs, prenons donc une minute pour les examiner :
hx-post="/send": Il fera une requête POST au/sendpoint final.hx-trigger="click from:#submitButton": Cela signifie que la requête sera déclenchée lorsque lesubmitButtonest cliquéhx-target="#chat": Ceci indique au navigateur où placer la réponse HTML. Dans ce cas, nous souhaitons que la réponse soit ajoutée à la liste.hx-swap="beforeend": Le hx-target indique où mettre le contenu, le hx-swap indique COMMENT. Dans ce cas, nous souhaitons que le contenu soit ajouté avant la fin (donc après le dernier enfant)
Le hx-on::before-request est un peu plus complexe, mais peut être expliqué facilement. Cela se produit essentiellement entre le clic et le moment où la demande est envoyée. Cela ajoutera l’entrée utilisateur au bas de la liste et effacera l’entrée utilisateur. De cette façon, nous obtenons une expérience utilisateur rapide !
Un meilleur chat (streaming + LLM)
Ce que nous avons construit est un chat très simple mais fonctionnel, cependant si nous voulons brancher un LLM, il se peut que la réponse du serveur prenne parfois beaucoup de temps. La façon dont notre chat actuel est construit est synchrone, ce qui signifie que rien ne se passera tant que l’écriture du LLM n’est pas terminée. Pas une excellente expérience utilisateur.
Ce dont nous avons besoin maintenant, c’est du streaming et d’un véritable LLM avec qui discuter. Et c’est la partie 2.



