---
name: htmx
slug: htmx
type: feed
source_url: https://htmx.org/atom.xml
organization: htmx
organization_slug: htmx
total_releases: 84
latest_date: 2026-06-11
last_updated: 2026-06-13
tracking_since: 2020-05-15
canonical: https://releases.sh/htmx/htmx
organization_url: https://releases.sh/htmx
---

<Release date="June 11, 2026" published="2026-06-11T00:00:00.000Z" url="https://htmx.org/essays/universities-and-ai/">
## The University In The AI Era

As I mentioned in [“Yes, And”](https://htmx.org/essays/yes-and/), I teach computer science at [Montana State University](https://www.cs.montana.edu).

In that earlier essay, I say that computer science is probably still a reasonably good area to study, but that you should also expand your skills beyond “just” computer science to help make yourself more employable in the future.

In this essay I want to think more about what AI means for universities in general and computer science programs in particular.

*Note: I apologize that this is a longer essay. I have provided a Table of Contents to help you navigate it.*

  

# Table of Contents

- [First: Is The University Still Relevant?](https://htmx.org/essays/universities-and-ai/#first-is-the-university-still-relevant)
- [Writing Code](https://htmx.org/essays/universities-and-ai/#writing-code)
- [Signaling Competence in an AI World](https://htmx.org/essays/universities-and-ai/#signaling-competence-in-an-ai-world)
- [Towards An AI-accepting CS Curriculum](https://htmx.org/essays/universities-and-ai/#towards-an-ai-accepting-cs-curriculum)
    -   [Current Changes](https://htmx.org/essays/universities-and-ai/#current-changes)
        -   [Homework Is No Longer A Strong Signal](https://htmx.org/essays/universities-and-ai/#homework-is-no-longer-a-strong-signal)
        -   [Homework Can Be More Ambitious & Realistic](https://htmx.org/essays/universities-and-ai/#homework-can-be-more-ambitious-realistic)
        -   [AI is a Great TA](https://htmx.org/essays/universities-and-ai/#ai-is-a-great-ta)
        -   [The Return of Butt-in-chair, Handwritten Tests](https://htmx.org/essays/universities-and-ai/#the-return-of-butt-in-chair-handwritten-tests)
        -   [Demos & Visualizations Are Cheap](https://htmx.org/essays/universities-and-ai/#demos-visualizations-are-cheap)
        -   [Class Content Should Be In Markdown](https://htmx.org/essays/universities-and-ai/#class-content-should-be-in-markdown)
        -   [Class Analysis & Improvements](https://htmx.org/essays/universities-and-ai/#class-analysis-improvements)
        -   [Automate *Everything*](https://htmx.org/essays/universities-and-ai/#automate-everything)
    -   [Upcoming Changes](https://htmx.org/essays/universities-and-ai/#upcoming-changes)
        -   [Stronger Pseudocode Standards](https://htmx.org/essays/universities-and-ai/#stronger-pseudocode-standards)
        -   [AI & Non-AI Tracks](https://htmx.org/essays/universities-and-ai/#ai-non-ai-tracks)
        -   [Open Source Work](https://htmx.org/essays/universities-and-ai/#open-source-work)
        -   [Clearly, Honestly Communicating The Dangers of AI](https://htmx.org/essays/universities-and-ai/#clearly-honestly-communicating-the-dangers-of-ai)
    -   [Speculative Changes](https://htmx.org/essays/universities-and-ai/#speculative-changes)
        -   [The “CS+” Concept](https://htmx.org/essays/universities-and-ai/#the-cs-concept)
        -   [Network Isolated Computers](https://htmx.org/essays/universities-and-ai/#network-isolated-computers)
        -   [Interview-Based Grading](https://htmx.org/essays/universities-and-ai/#interview-based-grading)
- [Conclusion](https://htmx.org/essays/universities-and-ai/#conclusion)

  
  

# First: Is The University Still Relevant?

An initial question that many people are asking is: in the era of AI, is the University still relevant?

This is not a new question. Many people have pointed to famous software industry figures who dropped out of college as proof that a university education isn’t useful in technology. And most people who have worked in Silicon Valley know at least one excellent engineer who either dropped out or simply never went to college.

So a college degree has never been a hard requirement for a successful career in technology. But, in reality, *most* software engineers have some sort of college under their belt and many of the best developers have studied computer science in their undergraduate education.

That being said, there is [clearly an emerging crisis](https://www.dailycal.org/news/campus/academics/failing-grades-soar-as-professors-see-greater-ai-usage-dwindling-math-skills-in-uc-berkeley/article_16fad0bf-02cb-4b8c-8d88-888ffd9f8608.html) in Computer Science education that needs to be addressed in order to keep the university relevant in the post-AI world.

# Writing Code

Historically, many computer science departments have looked at writing code as a secondary skill, to be picked up by students on their own, while the department focuses more on the theoretical foundations of computer science.

Since I was mature enough to have an opinion on the matter, I have viewed this as wrongheaded: I think you need to learn how to write code in order to appreciate those deeper theoretical foundations of computer science. If you can’t code up a linked list or use a hash table effectively, learning about the big-O behavior of them is much more abstract and difficult to grasp.

Ironically, in the era of AI, many *professional* environments are *also* starting to look at raw coding somewhat skeptically, sometimes insisting that their own engineers not write code at all, but rather use agents to generate it.

This approach may work for more experienced seniors, who have already written a lot of code and know what reasonable code looks like, but it puts junior developers in a bind: they don’t have pre-AI experience writing code, and now they are going into environments where no one is writing code.

As I said in “Yes, And”, you must write the code if you want to develop the ability to read code.

How is that supposed to happen at companies where nobody is writing the code?

I think this presents an opportunity for Computer Science departments: *we* can be the places where young software engineers write the code. By refocusing our curriculum on practical, code heavy assignments we can give students a safe environment, free of the time pressures and demands of corporate work, to write the code.

This experience can then put them in position to go into environments that use AI more heavily with the confidence that they know how to code and, because of that, are in a position to read and understand the code necessary for their career.

# Signaling Competence in an AI World

Now, of course, students are famously lazy and famously clever in figuring out how to be lazy. So, many students will use AI to complete many of these code-heavy assignments. They will learn very little or nothing, but will get a good grade because, let’s be honest, AI can perform at or above the level required for most reasonable undergraduate projects.

Here another irony of the AI era becomes evident: Universities are now in a position to signal competence in a way that nearly no other institution can. AI has made online testing pointless. I know this because the last semester I offered online tests (which I like to do because it is convenient for my working students) the testing scores were through the roof.

While I feel I am a pretty good teacher, this was clearly a case of AI being used by my students, despite my pleas.

When thinking about what I could do about this I realized that we had all the infrastructure for the perfect answer: in person, on paper testing. Universities have large lecture halls, expensive printers, testing centers for people who need additional help, etc.

Previously, I would have scoffed at this infrastructure as antiquated. But now I see that it puts me in a position to *more accurately establish* the competence of my students in a way that is difficult to game: I offer in-person quizzes, with one page of handwritten notes and no digital equipment, roughly every three weeks of my courses.

Of course students can still cheat, but the quizzes are proctored and now at least they have to work for it.

This in-person, on-paper testing infrastructure puts universities in a unique position to provide a high signal-to-noise indication of the competence of their students to the outside world.

# Towards An AI-accepting CS Curriculum

While I believe that the University CS degree is not only still relevant, but perhaps of *more* value than it was in the pre-AI era, I do think that significant changes need to be made to adapt to the new state of affairs. In this section I will describe what I have done over the last year with my courses, what I plan to do in the near future & then finish with some more speculative changes that I believe would help increase the usefulness of undergraduate CS degrees.

## Current Changes

First, let’s start with the changes that I have already made to my courses over the last year to deal with the new AI reality.

### Homework Is No Longer A Strong Signal

As with take home quizzes, due to the use of AI, homeworks & projects are no longer a strong signal of a students understanding of material.

Homeworks must become for the student’s benefit, opportunities for them to learn the art of writing code, rather than for evaluating their competency.

This is actually a good thing: homeworks can be more ambitious and the students that want to learn will have more opportunities to write more advanced code.

Yes, some (many?) will cheat on assignments, but the good students will have an opportunity to write code in a supportive environment.

To address this fact, I have reduced the weight of assignments in my classes from 60-80% (I have always had code-heavy classes) down to 50%, and I expect most students will get A’s on most assignments.

### Homework Can Be More Ambitious & Realistic

Another homework related change that I have made is that my assignments are now more ambitious and realistic. I don’t mean they are much harder.

Insead, what I mean is that I can, with the help of AI, present much larger software systems to my students with better sample data, as a basis for their projects.

This allows students to see software systems that go beyond “Hello World” levels of complexity and to develop the ability to navigate, read and write code in a larger, more complicated and realistic context.

### AI is a Great TA

Another thing I have found in the post-AI era is that my office hours traffic has dropped precipitously. I have always done my office hours in the computer lab on campus and, particularly for my compilers class, expected large crowds of students to come in asking for help on projects.

I think, unfortunately, this is most likely due to many students using AI to solve their programming problems.

However, there is a more optimistic read here: the students are using AI to better understand the projects and therefore do not need as much one on one help.

While I am ambivalent in many ways towards AI, this is an area where AI can significantly improve the university experience for students: with proper use, AI can be a *fantastic* TA. It is infinitely patient, has no other students waiting in line or it’s own classes to attend to and it is usually very competent at undergraduate level concepts in computer science.

The danger, of course, is that students simply use AI as a code generator to complete assignments and head off to the bars.

To address this danger, I ship a [`CLAUDE.md/AGENTS.md`](https://gist.github.com/1cg/a6c6f2276a1fe5ee172282580a44a7ac) file in my class repos that directs AI agents to act like a good TA rather than a code generator.

Of course students can modify or delete this file, but there is no system so perfect that no one needs to be good.

Stanford University has recently [modified this file for one of their own classes](https://github.com/stanford-cs336/assignment1-basics/blob/main/CLAUDE.md), and I encourage other people and departments to do the same: it is public domain.

### The Return of Butt-in-chair, Handwritten Tests

As I discussed above, Universities have infrastructure for in-person testing that make them uniquely qualified to assess expertise and competence in the post AI world.

I have switched to all in-person quizzes, roughly every three weeks. The three-week cadence gives enough time to cover a significant amount of material, even if holidays are interspersed in those weeks, while de-escalating each quiz when compared to a traditional midterm/final setup.

I also allow one page of handwritten notes. I do not allow printed notes. The idea here is to force the knowledge through the student’s eye-brain-hand pathway multiple times in order to help reinforce it.

My students have grumbled about this process, but also admit that it works in helping them learn the material.

My questions are all written response, never multiple choice. Sometimes I ask for prose, sometimes I ask for pseudocode, sometimes I will provide code and ask students to annotate/explain it, etc. This makes it harder to grade the tests, but also makes it much harder to cheat.

I have found that AI is very good at suggesting questions based on class material for quizzes. I will work with an AI agent based on my class slides (see below) to create appropriate quiz questions and then create a quiz review sheet to help students study for the quiz based on it.

Students love the review sheet because it helps them focus their studing efforts.

I think that, from a learning perspective, the butt-in-chair quizzes have been the single most positive change I have made to my classes. I now make quizzes 50% of a student’s grade, and my class grading curve has returned to a reasonable shape.

### Demos & Visualizations Are Cheap

Another adjustment I have had to make is that demos & visualizations are now very cheap to create with AI.

For a long time I was unhappy with the computer emulators that were available to me to teach my [computer systems](https://www.montana.edu/cope/activity_insight/catalog.php?rubric=CSCI&number=366) class. I wanted a 16-bit computer that struck a balance between the simplicity of something like [The Scott CPU](https://www.youtube.com/playlist?list=PLnAxReCloSeTJc8ZGogzjtCtXl_eE6yzA) and the full complexity of something like [SPIM](https://spimsimulator.sourceforge.net/).

Two summers ago we spent an entire summer building such a computer, called [The Montana Mini Computer](https://mtmc.cs.montana.edu/) that provided strong visualizations of how low level computing works.

Unfortunately, when I got into a class using it, I realized that the architecture I had picked was too exotic (mixing concepts from MIPS & the JVM) and that students would be better off learning an assembly closer to x86. x86 would be particularly useful later in our security classes.

I was able to work with AI to produce a new MTMC that was much closer to x86 in only a few weeks. It was so successful that I switched to the new version of the MTMC mid-class in fall, and used it exclusively in spring.

Another visualization that I have created with AI is a JVM emulator that shows how stack frames and the operand stack work together to do computation. It is a visualization that I always wanted, but was unable to create. This was not due to lack of skill on my part (I am a reasonably competent programmer) but just lack of time and energy (I am old.)

So I have had to reset my thinking on demos & visualizations: If you can think of a demo and describe it, and if you are a reasonably good programmer, you can probably create it.

### Class Content Should Be In Markdown

I have moved all my class slides to Markdown, using a tool called [slidev](https://sli.dev/) and, generally, embraced [Markdown](https://www.markdownguide.org/) for all my class content: SYLLABUS.md, etc. (Previously I was using Google slides for my lectures.)

All Markdown content is checked in to my class repository that students get.

Moving all my content to Markdown has been tremendously beneficial:

- I can run AI analysis over my slides and look for gaps or inconsistencies
- Students can run the content through an AI agent to create a more effective TA
- It is much easier to bulk-update my slides if I make a major change to a class

I have always liked Markdown and, with slidev, I have nice syntax highlighting and access to [Mermaid](https://mermaid.ai/open-source/intro/) for technical diagrams. Or I can use good ol’ ASCII art, which is often very effective.

Having everything locally in text/Markdown makes it much easier for AI tools to work effectively in my classes. As I mentioned above, AI agents can easily look at my class slides and suggest quiz questions I might ask.

### Class Analysis & Improvements

Another way I have used AI effectively, again enabled by moving everything to Markdown, is in reviewing my classes at a higher level:

- I can compare my classes with courses offered at other universities to see if there are topics that I am missing
- I can analyze my classes holistically and ensure that there are coherent threads between them (e.g. stack machines)

While this hasn’t revolutionized any of my classes I feel it has been useful in improving them.

### Automate *Everything*

The final way that I have been using AI to improve my classes is in automating everything possible. I have always had a significant number of scripts that I use for the infrastructure in my classes: an autograder.py that runs the autograding for project checkpoints in CI, etc.

I have become much more aggressive in what I will automate and optimize now.

For example, I am using [Tampermonkey](https://www.tampermonkey.net/) to make parts of [Canvas LMS](https://www.instructure.com/canvas) easier to work with for my work flows:

- I can paste in a youtube URL, and it will automatically create a link for me in Canvas
- I can drag and drop files directly from my OS into Canvas

You can see the tampermonkey script [here](https://gist.github.com/1cg/b5ed907a53eee2faa0bd6d079eeadb17)

I have also created command line scripts for scheduling new Youtube streams, parsing our autograder output into Canvas compatible format, etc.

At this point, if there is friction somewhere in my class I try to think how I would remove it if I had the time, then consider if an LLM could generate that solution.

## Upcoming Changes

I plan on implementing the following changes in the upcoming semester.

### Stronger Pseudocode Standards

With on-paper quizzes becoming a standard, it has become clear that I need a strong pseudocode standard for students to use on quizzes.

We are working on an “executable” pseudocode, [Notch](https://notch.cs.montana.edu), to address this. It is english-like (it is an [xTalk](https://en.wikipedia.org/wiki/HyperTalk) variant) and uses standards from Java, so the students should be able to pick it up easily. We will of course be lenient on syntax when it is used as pseudocode.

I intend to provide a pseudo-code guide that students are allowed to bring to quizzes for reference.

We will see how it goes.

### AI & Non-AI Tracks

In many of my classes I will have students give end-of-semester presentations and I often offer a reward for the best in show. In the upcoming year I am splitting these presentations into two tracks: AI & non-AI.

This will allow students who do not want to use AI to compete with one another and, I hope, encourage more students to not use AI for their projects.

I will stress that I will review the non-AI winner’s code base and, if I sniff any AI, they will be heavily penalized.

### Open Source Work

AI is disrupting Open Source work significantly. It changes the calculus on [build vs buy](https://www.thoughtworks.com/content/dam/thoughtworks/documents/e-book/tw_ebook_build_vs_buy_2022.pdf) dramatically in favor of build. This is made more compelling by people recognizing that dependencies [are liabilities](https://x.com/htmx_org/status/2057205905222246455), especially from a security perspective.

Of course much of the AI model training set consists of open source work and many open source developers are [understandably upset about this](https://opensourceainews.com/ai-is-destroying-open-source-not-even-good-yet/).

Chad Whitacre, an open source advocate who I respect tremendously, has decided to [step away from technology entirely](https://openpath.quest/2026/i-am-retiring-from-tech-to-live-offline/) due to the situation.

I do not have any good answers of how to prevent AI models from using open source work for training, nor do I have a good answer for financing open source work in general.

However, one possibility that I see is that universities become more explicitly involved in open source work, by forming open source groups. We have done so at [Montana State](https://opensource.cs.montana.edu) to support tools we use in our classes.

Universities have independent financing that allows them to pursue projects with steady (if unglamorous) levels of financial backing. This also dovetails with the public mission of many universities. Montana State, for example, is a land grant university, founded for “the advancement of agriculture, mechanical arts and military tactics.”

Leaving aside military tactics, open source is one way that public universities can contribute to the public good in a meaningful way.

### Clearly, Honestly Communicating The Dangers of AI

This upcoming semester I am going to spend more time communicating the dangers of AI to my students. The most obvious short term danger is that [they won’t write the code](https://htmx.org/essays/yes-and/#juniors) and, therefore, will not learn the skills needed to read the code.

They may get good grades on assignment, but my quizzes will be difficult and, when they enter the real world, they will not be able to work effectively, either with or without AI.

There are also studies showing that AI is [stultifying](https://www.media.mit.edu/projects/your-brain-on-chatgpt/overview/) and [homogenizes away creativity](https://www.nature.com/articles/s41562-025-02173-x).

I have been telling my students for many years now that they face far, far more temptations to behave poorly than any generation before them. That they face more difficult temptations in a week than their grandparents generation faced in years and perhaps decades.

With AI we now have another dimension, automated cheating, along which they will need to exercise even greater virtue than previous generations.

I stress to them that I admire the heroism of their generation in resisting these temptations.

(I think older generations would do well to recognize this fact.)

## Speculative Changes

Finally, I want to give a few concepts that I have thought of given an unlimited time & money budget.

### The “CS+” Concept

An idea proposed by MSU professor [Laura Stanley](https://www.cs.montana.edu/directory/1524633/laura-stanley) is “CS+”: integrating computer science education *plus* other majors in a meaningful way. Studies have shown that this increases the appeal of computer science more broadly to the student body.

I think this is a fantastic idea and will be working with our department to help re-orient our minor towards this concept. My hope is that we can refocus the minor and the first two years of our major program towards practical problem-solving with computers.

Perhaps AI will reduce the number of computer scientists needed in the world (I am skeptical of this claim) but it will certainly not reduce the need for technically adept students across the economy, both public and private. I think we can present a strong case to non-CS majors: “You can be a good X, but add CS and you will be among the best!”

I think this program can obviously appeal to engineering, science and technical majors such as economics, etc. However, I also think it is an opportunity to expand into liberal arts and social sciences. I can imagine a “CS+” minor being extremely useful for a sociology major, for example, and hope we can reach out to students in those departments once we have established the shape of our program.

This may not just be a matter of my own preferences (I admit I like this idea conceptually, not just practically): if CS departments do not consider this sort of program they may see falling enrollment as students avoid the CS major due to AI fear.

### Network Isolated Computers

I think it would be very forward-thinking for computer science departments to create network isolated computer labs.

These systems could be used to assess student competency while still providing a nice computing environment with IDEs, etc. without the risk of AI-generated solutions.

I can imagine assignments and quizzes that utilize this resource quite effectively. It would be a large investment and require management and upkeep, but could be very valuable if done well and perhaps become a centerpiece of a department’s teaching strategy.

### Interview-Based Grading

Finally, another way to avoid AI poisoning the evaluation of students is, again, returning to the past: sitting down and having [a conversation](https://en.wikipedia.org/wiki/Oral_exam) with a student to determine their grade.

While I do not have experience directly with oral exams, my experience in office hours tells me that I can determine the competence of a given student in roughly five minutes of conversation, guiding the conversation to the level that they are comfortable with, giving hints where necessary, etc. and determine a reasonable grade for them in a manner that would be very hard to game.

Now, many of my classes have 100+ students, and if it took, say, 15 minutes per student interview, that is 25 hours of total interview time. This is not realistic given the current structure of classes (I currently have 3 hours of lecture over 15 weeks, or 45 total hours.)

However, a forward-looking university might restructure finals week (or perhaps two finals weeks) in such a way to allow oral exams, and it would provide a much better final analysis of students’ achievement in a class.

(Of course it would be a lot more work for professors, so I view this as unfortunately unlikely.)

# Conclusion

So, yes, I think The University is still relevant in the post-AI era and, in fact, may become *more* relevant due to some structural advantages it has, particularly in signaling student competence to the outside world.

I do think computer science departments will need to adjust to the new realities and consider some somewhat radical changes in order to maximize their value to their students. Most importantly, I think universities should increase their focus on providing hand-coding opportunities to students, since these opportunities are becoming less available after students leave school.

I hope that this essay helps computer science educators improve their course offering so that we can continue to produce competent and confident computer scientists for the foreseeable future.

*Note: No AI was used in the writing of this essay. AI was used to correct typos and to produce the table of contents.*
</Release>

<Release date="June 4, 2026" published="2026-06-04T00:00:00.000Z" url="https://htmx.org/essays/code-is-cheap/">
## Code is Cheap(er)

There is no getting around the fact that, in the last year, code has gotten much cheaper to create. AI is able to generate reams and reams of code, often of reasonably decent quality, incredibly quickly. There is no point in pretending that this isn’t the case.

At times, when confronted with this admittedly uncomfortable fact, I have seen people I respect say something like “coding was never the problem.”

While I appreciate the sentiment, I don’t completely agree with that: certainly coding was at least *part* of the problem.

And that part of the problem has shrunk significantly with the advent of effective AI coding tools.

So what does raw coding becoming less important mean for software developers, people who, in the past, prided themselves (and often compared themselves) on their ability to code?

## Understanding is Expensive(er)

One thing I see is that it means that *understanding* code has become more expensive. This is because when reams and reams of code are generated, rather than emerging painfully from a particular programmer’s fingers, there *is* no understanding of that code.

In as much as understanding that code needs to exist, it has to be done after the code is written, by reading the code.

Note that conventional wisdom is that reading someone else’s code is harder than writing your own code.

Some AI enthusiasts say “Who cares, you don’t understand the output of compilers.”

I think that is a category error for multiple reasons:

- Compilers are deterministic; LLMs are, by design, not
- Compiler workflows retain their original source code; LLM workflows typically do not
- Compiler output is to a narrowly constrained domain (machine code); LLM output is not (generalized software)

I maintain that, in most cases and certainly for mission-critical software, developers still need to understand the underlying code even if it is generated by an LLM.

And if code *is* generated by an LLM there is a stark danger: the LLM can produce code far faster than you, or anyone else, can understand it. This is why I recommend incremental use of LLMs rather than allowing them to generate massive changelists that neither you, nor anyone else, can understand.

(There are times when this can be appropriate, such as in a mechanical refactor, but it is extremely dangerous when new semantics are being introduced into a code base.)

## The Sorcerer’s Apprentice Trap

One movie scene that has been consistently coming back to me as I have watched AI garner more and more attention is [The Sorcerer’s Apprentice](https://video.disney.com/watch/sorcerer-s-apprentice-fantasia-4ea9ebc01a74ea59a5867853) from Disney’s movie *Fantasia*.

In this scene the apprentice decides to use magic to assist in the drudgery of cleaning. He enchants a broom which then proceeds to start cleaning things up. Things appear to be going swimmingly for a while, until the broom starts cleaning more and more vigorously, reaching a point where things start going swimmingly literally.

The chaos is resolved when the Sorcerer reappears and asserts control over the situation, glaring at the apprentice for his foolishness.

This seems like an apt metaphor for the AI era: you want to be a sorcerer and not an apprentice.

And a sorcerer has to understand the code.

## Complexity: Still Bad

Humans, generally, have a poor grasp of geometric and exponential curves.

(This is why they believe in fairy tales such as compound interest.)

The core danger of code being cheap is [complexity](https://grugbrain.dev), which I assert, without proof, tends to grow at least geometrically and often exponentially with the size of a system.

Before LLMs there were prolific human coders.

Perhaps you have worked with one: they can write *a lot* of code.

I have seen prolific coders who lack a proper fear of complexity heap more and more code on top of an existing problem until the whole system collapses into an unmodifiable steady state, where any change creates as many bugs as it fixes.

LLMs are incapable of fear of complexity, and are prolific coders.

Seems dangerous to me.

## The Subtractive, Constraining Engineer

To address this danger of LLM-generated code, I propose the subtractive, constraining engineer:

This engineer [says no](https://grugbrain.dev/#grug-on-saying-no), closely examines LLM output, suggests simplifications and generally retains a firm hand when dealing with LLM-generated code.

Rather than priding themselves on the code they create, they pride themselves on the code (and layers) they *remove* from or prevent from entering systems.

This ethos is more sculptor and less builder.

Where the builder ethos still applies, to an extent, is at the system design level: a good engineer will need to know how to compose components effectively to create systems. However, even here, I think that the subtractive mindset will be useful: removing unnecessary components and system boundaries to simplify system deployment and inter-component interactions, etc.

The subtractive engineer is a different kind of engineer than most coders have been in the past. I will admit that I have always been sympathetic to the subtractive engineer mindset: I don’t mind saying no, I don’t mind polishing existing systems rather than heroic rewrites, etc.

But, admitting my own biases, I believe this approach is a productive way to engage with LLMs that retains the art of computer programming and properly acknowledges a dual reality: code has gotten much cheaper to create and complexity remains our apex predator.
</Release>

<Release date="March 18, 2026" published="2026-03-18T00:00:00.000Z" url="https://htmx.org/essays/mcp-apps-hypermedia/">
## Hypermedia Friendly Model Context Protocol App Architecture

I am working on [speedystride.com](https://speedystride.com), a programming tool that helps athletes quickly input
workouts on their Apple and Garmin watches.

These watches come with a built-in workout programming feature that is especially useful for structured programs. For
example, runners will often do interval training, which could be something like 5x1000m with 2 minute rest.

And sometimes they’ll want to do a fartlek (Swedish for ‘Speed Play’) where they will vary their speed: run 400 meters
fast - run 800 meters slower - sprint 200 meters.
These smartwatches will vibrate and beep to help the user perform at the desired target, and also count down rest
periods so the user is rested
enough for that next hard interval.

Unfortunately, I did not like any of the first party workout builders. These are form-based, with a drag-and-drop
interface to structure your workouts.
I think these builders have a high user friction; more user inputs are required in proportion to the output.
Additionally, these
builders [run on a small watch screen](https://support.apple.com/en-gb/guide/watch/apd66fcd5c5c/watchos),
or require a separate app.
This is less than ideal when you are trying to program your watch right before a track workout. There are third party
tools in this space, but as far as I can tell, they do not fundamentally break this pattern.

I also wanted to share these workouts with everyone at my city’s track club. My service provides a scheduler that pushes
workouts automatically at a specified time for our club training; about 90% of our members have either an Apple Watch
or a Garmin so cross-platform compatibility is a very important factor.

To solve these problems, I came up with a very simple domain specific language (DSL) for both people and machines.
It can describe exercises, define rest times, and combine everything together into repeat intervals.
I implemented a simple recursive descent parser, and it outputs data formats for both Apple and Garmin devices.
By defining a small language, I was able to avoid implementing complex forms unlike the current offerings. User input is
reduced
to plain text.

### Example workout DSL

User:

```
10x200m max effort with 2 minute rest

```

DSL:

```
Repeat 10 times:
- Run 200m @RPE 10
- Rest 2 minutes

```

I had initially wanted coaches to learn this DSL and enter programs into my website assisted by a Codemirror editor.
I incorrectly thought that it was close enough to English for people to quickly learn it when assisted by autocomplete
features.
I was not meeting my users where they were at; graybeard track coaches had zero interest in learning how to program.
What I needed
was a translator that could convert natural English into my DSL.

## Model Context Protocol (MCP) and MCP Apps

As I started sharing my project with other people, large language models were becoming popular, and it was an obvious
tool to translate natural English workouts into my training DSL.
By integrating LLMs, I can massively reduce user friction. There are no forms with complex UIs to implement. There is no
DSL to learn as the AI can translate natural
language for you. Users can now express their workouts in their own way.

An AI can transform the above user input to this Domain Specific Language with a relatively small language
specification. There was also a nice side effect of being very token efficient.
JSON payloads defining a repeated workout set can get quite large while my DSL can stay compact.
Any errors can be corrected as my parser can provide rich feedback on what went wrong. I have found that 95% of interval
workouts I see can be expressed through my language.

LLMs also enable new capabilities, such as programming your watch from a photo of a whiteboard.
Even more importantly, Model Context Protocol (MCP) was starting to gain traction. MCPs are a way for LLM systems to
interact with the real world, which means that besides
just outputting workout programs, the LLM can call a remote function to actually send that workout to your device.

Anthropic and OpenAI both support MCP. So it would be awesome for my business to support LLM integrations since so many
users already have Claude and ChatGPT installed on their phone.

Still, there were opportunities to further improve user experience.

I had mentioned earlier that my track club uses speedystride.com to program members’ watches. In order to do so, we have
to define a few parameters:

- What is the workout?

- Should the workout be added to my Monday night track intervals, or for Tuesday fartleks?

LLMs can help massively with the first question. But how about the second? Much of the current human-to-LLM interaction
is text-based, and MCPs are no exception.
Besides improving workout building UX, AI tools introduced new frictions. To associate a workout with an event, I had an
events tool that would fetch upcoming events for the user and add them
to the LLM context. Then it was up to the LLM to guide the user. Some systems like Claude do provide simple select
controls if your tools output JSON objects that look like a set of choices.
However, this interaction forces the developer to surrender control of the happy path, which often leaves users
confused. Also, the AI would sometimes try to be too helpful and just guess the tool inputs.
In summary, back and forth conversation with the LLM is not an ideal UX as the users have to figure out how to guide the
AI to the right inputs.

A form with a selector interface is an obvious way to solve this problem.

Luckily for me, the new MCP Apps specification was released in January 2026. This is an extension to the MCP
specification
that allows rendering custom UI inside an `` of the MCP Host.

References:

- [MCP](https://modelcontextprotocol.io/docs/getting-started/intro)

- [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview)

### MCP App Architecture

You need to host an MCP server that can communicate with the AI systems.

#### Communication model:

```
MCP Server  LLM Host (Claude or ChatGPT)  MCP App UI (rendered inside LLM )

```

All traffic between the MCP App UI and the LLM host must be routed through the App Bridge. The LLM host will then make
proxied requests to my MCP server.

### Interactive Hypermedia UIs in MCP Apps

Let’s say that we are developing a simple workout scheduler, where we have the LLM generated workout program. Our goal
is to
associate this workout with a calendar event occurrence.
A user could have multiple events on her calendar, so a dynamic choice of occurrences should be available for each event
she is subscribed to.
On a traditional website, this could be trivially handled by a full page refresh.

On MCP App systems without interactivity, we would have to ask the LLM to fully render the MCP App
in the chat. This adds friction and unnecessarily consumes tokens. So we must find a path to interactivity within the
same UI context.

There are existing UI toolkits, such as [MCP-UI](https://mcpui.dev) that works really well with React. However, using
React for a simple `` felt too complex, so I wanted a hypermedia solution.

I initially considered using HTMX, since my HTTP site uses it already. The challenge was that HTMX is heavily geared
towards processing HTTP requests.
Since my MCP App UI renders inside an ``, I don’t need to push URLs or manage history.

Then I remembered [Fixi](https://github.com/bigskysoftware/fixi) library from the creator of HTMX. It is a minimal
version of generalized hypermedia controls. Fixi proved to be very useful precisely because it doesn’t do much.
In addition to lacking history or URL push features, I can also copy & paste Fixi to my project to skip build steps. I
also don’t have to worry about CSP configuration if my Fixi code is served with my HTML.

The killer feature of Fixi is its [hackability](https://github.com/bigskysoftware/fixi?tab=readme-ov-file#mocking).
Because it expects a Response compatible object (or a Promise) rather than a strict network request, we can entirely
bypass HTTP.
I can configure Fixi to call `app.callServerTool` when it sees any `fx-action` that starts with `tool:`. If I use a
``, its inputs will become the function args.

Let’s examine how to architect MCP features to serve hypermedia.

#### MCP App Lifecycle

From [MCP docs](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#architecture):

- Create: Instantiate App with info and capabilities

- Connect: Call `connect()` to establish transport and perform handshake

- Interactive: Send requests, receive notifications, call tools

- Cleanup: Host sends teardown request before unmounting

We will focus on items 2 and 3 to demonstrate a hypermedia driven MCP App.

#### The MCP Server

##### MCP Server Architecture

I host my server on a Gunicorn + Uvicorn monolith. ASGI Django handles regular HTTP traffic, and MCP traffic is routed
to
FastMCP. Since both Django and FastMCP work together, I can share resources between the HTTP and MCP domains including
ORM and template rendering.

The LLM host will render a UI by calling an MCP **tool** and its associated **resource**.

A **tool** is similar to a view- it is a function that can return JSON or hypertext. Let’s say that our UI tool is
called `show_user_ui`.

A **resource** is a bit like a pointer to assets. LLM hosts can preload resources to deliver them more quickly to users.

Tools and resources are registered by `@mcp.tool` or `@mcp.resource` decorators. You could think of registration as
defining `urls.py` in Django.

Since our Django and FastMCP Applications live on the same server, we can render HTML with Django’s `render_to_string`
function with ORMs and templatetags.
Django 6’s
new [built-in template partials](https://docs.djangoproject.com/en/6.0/ref/templates/language/#template-partials) also
make
template organization and partial rendering easy. This allows us to co-locate our initial UI render and our dynamic
hypermedia fragments in a single file, keeping the MCP tool logic incredibly clean.

##### After lifecycle item 2: The initial MCP App render

Let’s talk about the **resource** first. It points to a HTML where our Fixi and MCP App Bridge code will be placed.

```
from django.template.loader import render_to_string

@mcp.resource(
    "ui://user_ui_resource.html",
    mime_type="text/html;profile=mcp-app",
    description="User UI resource",
)
async def user_ui_resource():
    return render_to_string("mcp/user_ui_resource.html")

```

This resource serves HTML rendered with `render_to_string` to resolve static files and template tags, but not any
user-specific data.

mcp/user_ui_resource.html

```

html>
head>
    {% include "fixi, app bridge source code, styles, etc" %}

    style>
        /* CSS indicator classes to toggle visibility during requests */
        #indicator {
            display: none;
        }

        #indicator.fixi-request-in-flight {
            display: inline-block;
        }
    style>

    script type="module">
        ...
        MCP
        App
        bridge
        setup
        ...

        // 1. Configure Fixi to route `tool:` fx-actions through MCP App bridge
        document.addEventListener("fx:config", (evt) => {
            const action = evt.detail.cfg.action;
            if (action.startsWith("tool:")) {
                const toolName = action.replace("tool:", "");
                console.log(`callServerTool: ${toolName}`);
                const args = Object.fromEntries(evt.detail.cfg.body ?? []);
                evt.detail.cfg.fetch = async () => {
                    const result = await app.callServerTool({name: toolName, arguments: args});
                    return {text: async () => result.structuredContent?.html};
                };
            }
        });

        // 2. Set up request indicator extension: ext-fx-indicator
        document.addEventListener("fx:init", (evt) => {
            if (evt.target.matches("[ext-fx-indicator]")) {
                let disableSelector = evt.target.getAttribute("ext-fx-indicator")
                evt.target.addEventListener("fx:before", () => {
                    let disableTarget = disableSelector === "" ? evt.target : document.querySelector(disableSelector)
                    disableTarget.classList.add("fixi-request-in-flight")
                    evt.target.addEventListener("fx:after", (afterEvt) => {
                        if (afterEvt.target === evt.target) {
                            disableTarget.classList.remove("fixi-request-in-flight")
                        }
                    })
                })
            }
        });

        // 3. Populate UI on initial render with dynamic content fetched with `show_user_ui` tool
        app.ontoolresult = (params) => {
            document.body.innerHTML = params.structuredContent?.html ?? document.body.innerHTML;
        };
    script>
head>

{# Empty body, since `app.ontoolresult` will populate it on load #}
body>body>

{% partialdef save-form-fragment %}
div id="save-form-fragment">
    Workout {{ workout.id }} has been saved for {{ workout.occurrence.event.id }} on {{ workout.occurrence.start_date
    }}.
div>
{% endpartialdef %}

{% partialdef main-contents %}
div id="main-contents">
    form>
        input name="workout_dsl" type="hidden" value="{{workout_dsl}}">

        select
                id="event-selector"
                name="event_id"
                fx-action="tool:render_occurrences_fragment"
                fx-target="#occurrence-fragment"
                fx-swap="outerHTML"
        >
            option>-----option>
            {% for evt in events %}
            option value="{{ evt.id }}">{{ evt.name }}option>
            {% endfor %}
        select>

        {% partialdef occurrence-fragment inline %}
        select id="occurrence-fragment" name="occurrence_id">
            {% for occ in occurrences %}
            option value="{{ occ.id }}">{{ occ.start_date }}option>
            {% endfor %}
        select>
        {% endpartialdef %}

        button
                type="submit"
                fx-action="tool:save_form_fragment"
                fx-swap="outerHTML"
                fx-target="#main-contents"
                ext-fx-indicator
        >
            span>Save Workoutspan>
            svg id="indicator">...svg>
        button>
    form>
div>
{% endpartialdef %}
html>

```

This HTML resource defines three core pieces of logic within its `` tag:

- Event listener for `fx:config`: We can use `fx-action` attribute to make MCP tool calls.

- Event listener for `fx:init`: Set up an indicator to show that a tool call is being processed.

- Handle `app.ontoolresult`: Display the main UI with the output by processing `CallToolResult`.

The LLM will load the **resource** in the chat, and then call `show_user_ui` **tool**, which renders the `main-contents`
template partial.

```
from django.template.loader import render_to_string

from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp import Context
from mcp.types import CallToolResult

from events.models import Events, Occurrences
from workout_validator import validate_program, DSLValidationError

mcp = FastMCP(...)

@mcp.tool(
    name="show_user_ui",
    description="Display UI",
    meta={"ui/resourceUri": "ui://user_ui_resource.html"}
)
async def show_user_ui(ctx: Context, workout_dsl: str) -> CallToolResult:
    user = await get_user_from_context(ctx)

    try:
        validate_workout = validate_program(workout_dsl)
    except DSLValidationError as e:
        ...
        handle
        invalid
        workout
        data...

    initial_events_qs = Events.objects.filter(user=user)
    initial_events = [evt async for evt in initial_events_qs.aiterator()]

    if len(initial_events) > 0:
        initial_occurrences_qs = Occurrences.objects.select_related("event").filter(
            event_id=initial_events[0]).order_by("start_date")
        initial_occurrences = [occ async for occ in initial_occurrences_qs.aiterator()]
    else:
        initial_occurrences = []

    template_context = {"user": user, "workout_dsl": workout_dsl, "events": initial_events,
                        "occurrences": initial_occurrences}
    rendered_html = render_to_string("mcp/user_ui_resource.html#main-contents", template_context)

    return CallToolResult(
        content=[TextContent(type="text", text="UI ready")],
        structuredContent={"html": rendered_html}
    )

```

Args for `show_user_ui`:

- `ctx`: object will allow us to authenticate our users, and is passed to our tool by the LLM host.

- `workout_dsl`: AI will generate and pass this parameter. We will render our HTML with this data stored in
`` and save it later.

Return values from `show_user_ui`:

- content: An array of text or other data to show the User/LLM

- structuredContent: Rendered hypertext

MCP’s `ontoolresult` handler will insert `structuredContent.html` into `` tag to render our initial UI.

Note that this `innerHTML` assignment is reasonably safe in our context. The only untrusted input here is AI generated
`workout_dsl`, and we run validation on it before rendering our template.

#### Lifecycle 3: Rendering Hypermedia Fragments

Now we have a full `` with two `` controls. How do we add interactivity for our event occurrence
selectors?
Every time the user changes `#event-selector` option, we should update our options shown in `#occurrence-fragment` with
a new tool.
We need to use template fragments to fetch occurrence options for different events without triggering another UI render.

Let’s first examine how Fixi will trigger a fragment request. Scroll back above and read `#event-selector` defined in
`#main-contents`.

We see these Fixi attributes:

- fx-action: Calls `render_occurrences_fragment` MCP tool. Fixi will bind `fx-action` to appropriate default events,
such as `change` for ``.

- fx-target: Insert `render_occurrences_fragment` tool’s HTML output in the `#occurrence-fragment` div

- fx-swap: Use `outerHTML` swap on `#occurrence-fragment`, which replaces this div instead of inserting contents.

Below shows occurrence HTML fragment rendering `occurrence-fragment` Django template partial. This tool’s output will
replace `#occurrence-fragment`.

```
from events.models import Occurrences

@mcp.tool(
    name="render_occurrences_fragment",
    description="Fetches event occurrences available to the logged in user",
    meta={"ui": {"visibility": ["app"]}}
)
async def render_occurrences_fragment(ctx: Context, event_id) -> CallToolResult:
    ...
    validate
    event_id and authenticate
    user...

    occurrences_qs = Occurrences.objects.select_related("event").filter(
        event_id=event_id
    ).order_by("start_date")
    occurrences = [occ async for occ in occurrences_qs.aiterator()]

    rendered_html = render_to_string("mcp/user_ui_resource.html#occurrence-fragment", {"occurrences": occurrences})
    return CallToolResult(
        text="Here are this user's event occurrences",
        structuredContent={"html": rendered_html}
    )

```

It does not make sense for LLM to fetch this HTML fragment by itself, so we can set
`meta={"ui": {"visibility": ["app"]}}` registration parameter to prevent unnecessary tool calling.

Finally, let’s submit this form. This action is triggered by ``, and all of the `` fields will be sent to
`save_form_fragment` as function args.

```
from workouts.models import Workout
from events.models import Events, Occurrences

@mcp.tool(
    name="save_form_fragment",
    description="Saves workout",
    meta={"ui": {"visibility": ["app"]}}
)
async def save_form_fragment(ctx: Context, workout_dsl, event_id, occurrence_id) -> CallToolResult:
    ...
    validate
    args and authenticate
    user...
    occurrence = await Occurrences.objects.select_related("event").aget(id=occurrence_id, event_id=event_id)
    workout = await Workout.objects.acreate(occurrence=occurrence, workout_dsl=workout_dsl)

    # Render the events fragment
    rendered_html = render_to_string("mcp/user_ui_resource.html#save-form-fragment", {"workout": workout})
    return CallToolResult(
        text="Workout saved",
        structuredContent={"html": rendered_html}
    )

```

The submit indicator is defined by configuring Fixi event `fx:init` to add `.fixi-request-in-flight` class to
`#indicator` SVG inside our submit button if it has `ext-fx-indicator` attribute.
After this, the interaction cycle is finished. If the user wants to make modifications, she can ask the AI to render a
new UI or visit the website to make quick changes.

## Demo

Here is a demo of this application in action:

![](/img/mcp_hypermedia_example.gif)

## Conclusion

I hope to have demonstrated a simple way to develop user interfaces that work well with AI.
MCP Apps introduce a new rendering environment, but hypermedia systems continue to work well in this context with some
modifications.

This is worth emphasizing because the current zeitgeist when building with AI is to reach for a client-side framework.
But the constraints of MCP Apps actually push in the opposite direction. Your `` cannot talk directly to your
server and every interaction must cross the bridge.
The less state and logic you pack into the client, the less surface area you have for things to go wrong across that
boundary.

Throughout my design process, I tried to channel Nintendo’s Gunpei Yokoi: What can we do with old-fashioned technology
using lateral thinking?
I was able to sidestep complex drag-and-drop form builders by combining a small DSL with AI and hypermedia.
Integrating MCP Apps solved the friction of coaxing the AI to select the right inputs, and using hypermedia made the
entire system almost trivially simple: forms, selectors, fragment updates, loading indicators.
Simplicity does not guarantee success, but I think that it will give me a better fighting chance.

Special thanks to Carson Gross for creating Fixi, HTMX, and Hyperscript, and for encouraging me to write this post.
</Release>

<Release date="February 27, 2026" published="2026-02-27T00:00:00.000Z" url="https://htmx.org/essays/yes-and/">
## Yes, and...

I teach computer science at [Montana State University](https://www.cs.montana.edu).  I am the father of three sons who
all know I am a computer programmer and one of whom, at least, has expressed interest in the field.  I love computer
programming and try to communicate that love to my sons, the students in my classes and anyone else who will listen.

A question I am increasingly getting from relatives, friends and students is:

Given AI, should I still consider becoming a computer programmer?

My response to this is: “Yes, and…”

## “Yes”

Computer programming is, fundamentally, about two things:

- Problem-solving using computers

- Learning to control complexity while solving these problems

I have a hard time imagining a future where knowing how to solve problems with computers and how to control the complexity
of those solutions is *less* valuable than it is today, so I think it will continue to be a viable career even with the
advent of AI tools.

### “You have to write the code”

That being said, I view AI as very dangerous for junior programmers because it *is* able to effectively generate code for
many problems.  If a junior programmer does not learn to write code and simply generates it, they are robbing
themselves of the opportunity to develop the visceral understanding of code that comes with being down in the trenches.

Because of this, I warn my students:

“Yes, AI can generate the code for this assignment. Don’t let it. You *have* to write the code.”

I explain that, if they don’t write the code, they will not be able to effectively *read* the code.  The ability to
read code is certainly going to be valuable, maybe *more* valuable, in an AI-based coding future.

If you can’t read the code you are going to fall into [The Sorcerer’s Apprentice Trap](https://www.youtube.com/watch?v=m-W8vUXRfxU),
creating systems [you don’t understand and can’t control](https://www.youtube.com/watch?v=GFiWEjCedzY).

### Is Coding → Prompting like Assembly → High Level Coding?

Some people say that the move from high level languages to AI-generated code is like the move from assembly to
[high level programming languages](https://en.wikipedia.org/wiki/High-level_programming_language).

I do not agree with this simile.

Compilers are, for the most part, deterministic in a way that current AI tools are not.  Given a high-level programming
language construct such as a for loop or if statement, you can, with reasonable certainty, say what the generated
assembly will look like for a given computer architecture (at least pre-optimization).

The same cannot be said for an LLM-based solution to a particular prompt.

High level programming languages are a *very good* way to create highly specified solutions to problems
using computers with a minimum of text in a way that assembly was not.  They eliminated a lot of
[accidental complexity](https://en.wikipedia.org/wiki/No_Silver_Bullet), leaving (assuming the code was written
reasonably well) mostly necessary complexity.

LLM generated code, on the other hand, often does not eliminate accidental complexity and, in fact, can add
significant accidental complexity by choosing inappropriate approaches to problems, taking shortcuts, etc.

If you can’t read the code, how can you tell?

And if you want to read the code you must write the code.

### AI is a great TA

Another thing that I tell my students is that AI, used properly, is a tremendously effective TA.  If you don’t use it
as a code-generator but rather as a partner to help you understand concepts and techniques, it can provide a huge boost
to your intellectual development.

One of the most difficult things when learning computer programming is getting “stuck”.  You just don’t see the trick
or know where to even start well enough to make progress.

Even worse is when you get stuck due to accidental complexity: you don’t know how to work with a particular tool chain
or even what a tool chain is.

This isn’t a problem with *you*, this is a problem with your environment.  Getting stuck pointlessly robs you of time to
actually be learning and often knocks people out of computer science.

(I got stuck trying to learn Unix on my own at Berkeley, which is one reason I dropped out of the computer science
program there.)

AI can help you get past these roadblocks, and can be a great TA if used correctly.  I have posted an
[AGENTS.md](https://gist.github.com/1cg/a6c6f2276a1fe5ee172282580a44a7ac) file that I provide to my students to configure
coding agents to behave like a great TA, rather than a code generator, and I encourage them to use AI in this role.

AI doesn’t *have* to be a detriment to your ability to grow as a computer programmer, so long as it is used
appropriately.

## “, and…”

I do think AI is going to change computer programming.  Not as dramatically as some people think, but in some
fundamental ways.

### Raw coding may become less important

It may be that the *act* of coding will lose *relative* value.

I regard this as too bad: I usually like the act of coding, it is fun to make something do something with your
(metaphorical) bare hands.  There is an art and satisfaction to writing code well, and lots of aesthetic decisions to be
made doing it.

However, it does appear that raw code writing prowess may be less important in the future.

As this becomes relatively less important, it seems to me that other skills will become more important.

### Communication Skills

For example, the ability to write, think and communicate clearly, both with LLMs and humans seems likely to be much more
important in the future.  Many computer programmers have a literary bent anyway, and this is a skill that will likely
increase in value over time and is worth working on.

Reading books and writing essays/blog posts seem like activities likely to help in this regard.

### Understanding Business

Another thing you can work on is turning some of your mental energy towards understanding a business (or government
role, etc) better.

Computer programming is about solving problems with computers and businesses have plenty of both of these.

Some business folks look at AI and say “Great, we don’t need programmers!”, but it seems just as plausible to me that
a programmer might say “Great, we don’t need business people!”

I think both of these views are short-sighted, but I do think that AI can give programmers the ability to continue
fundamentally working as a programmer while *also* investing more time in understanding the real-world problems (business or
otherwise) that they are solving.

This dovetails well with improving communication skills.

### “Architecting” Systems

Like many computer programmers, I am ambivalent towards the term “software architect.”  I have seen
[architect astronauts](https://www.joelonsoftware.com/2001/04/21/dont-let-architecture-astronauts-scare-you/) inflict
a lot of pain on the world.

For lack of a better term, however, I think software architecture will become a more important skill over time: the
ability to organize large software systems effectively and, crucially, to control the complexity of those systems.

A tough part of this for juniors is that traditionally the ability to architect larger solutions well has come from
experience building smaller parts of systems, first poorly then, over time, more effectively.

Most bad architects I have met were either bad coders or simply didn’t have much coding experience at all.

If you let AI take over as a code generator for the “simple” stuff, how are you going to develop the intuitions necessary
to be an effective architect?

This is why, again, you must write the code.

### Using LLMs Effectively

Another skill that seems likely to increase in value (obviously) is knowing how to use LLMs effectively.  I think that
currently we are still in the process of figuring out what that means.

I also think that what this means varies by experience level.

#### Seniors

Senior programmers who already have a lot of experience from the pre-AI era are in a good spot to use LLMs effectively:
they know what “good” code looks like, they have experience with building larger systems and know what matters and
what doesn’t.  The danger with senior programmers is that they stop programming entirely and start suffering from
[brain rot](https://www.media.mit.edu/publications/your-brain-on-chatgpt/).

Particularly dangerous is firing off prompts and then getting sucked into
[The Eternal Scroll](https://theneverendingstory.fandom.com/wiki/The_Nothing) while waiting.

Ask me how I know.

I typically try to use LLMs in the following way:

- To analyze existing code to better understand it and find issues and inconsistencies in it

- To help organize my thoughts for larger projects I want to take on

- To generate relatively small bits of code for systems I am working on

- To generate code that I don’t enjoy writing (e.g. regular expressions & CSS)

- To generate demos/exploratory code that I am willing to throw away or don’t intend to maintain deeply

- To suggest tests for a particular feature I am working on

I try not to use LLMs to generate full solutions that I am going to need to support.  I will sometimes use LLMs alongside
my manual coding as I build out a solution to help me understand APIs and my options while coding.

I never let LLMs design the APIs to the systems I am building.

#### Juniors

Juniors are in a tougher spot.  I will say it again: you must write the code.

The temptation to vibe your way through problems is very, very high, but you will need to fight against that temptation.

Peers *will* be vibing their way through things and that will be annoying: you will need to work harder than they do,
and you may be criticized for being slow.  The work dynamics here are important to understand: if your company
prioritizes speed over understanding (as many are currently) you need to accept that and not get fired.

However, I think that this is a temporary situation and that soon companies are going to realize that vibe coding at
speed suffers from worse complexity explosion issues than well understood, deliberate coding does.

At that point I expect slower, more deliberate coding with AI assistance will be understood as the best way to utilize
this new technology.

Where AI *can* help juniors is in accelerating the road to senior developer by eliminating accidental complexity that often
trips juniors up.  As I said above, viewing AI as a useful although sometimes overly-eager helper rather than a servant
can be very effective in understanding the shape of code bases, what the APIs and techniques available for a particular
problem are, how a given build system or programming language works, etc.

But you must write the code.

And companies: you must let juniors write the code.

## Getting a Job Today

The questions I get around AI and programming fundamentally revolve around getting a decent job.

It is no secret that the programmer job market is bad right now, and I am seeing good CS students struggle to find
positions programming.

While I do not have a crystal ball, I believe this is a temporary rather than permanent situation.  The computer
programmer job market tends to be cyclical with booms and busts, and I believe we will recover from the current bust
at some point.

That’s cold comfort to someone looking for a job now, however, so I want to offer the specific job-seeking advice that
I give to my students.

### Family, Friends, Family of Friends

I view the online job sites as mostly pointless, especially for juniors.  They are a lottery and the chances of finding
a good job through them are low.  Since they are free they are probably still worth using, but they are not worth
investing a lot of time in.

A better approach is the four F’s: Family, Friends & Family of Friends.  Use your personal connections to find positions
at companies in which you have a competitive advantage of knowing people in the company.  Family is the strongest
possibility.  Friends are often good too.  Family of friends is weaker, but also worth asking about.  If you know or
are only a few degrees separated from someone at a company you have a much stronger chance of getting a job at that
company.

I stress to many students that this doesn’t mean your family has to work for Google or some other big tech company.

*All* companies of any significant size have problems that need to be solved using computers.  Almost every company over 100
people has some sort of development group, even if they don’t call it that.

As an example, I had a student who was struggling to find a job.  I asked what their parent did, and they said they worked
for Costco corporate.

I told them that they were in fact extremely lucky and that this was their ticket into a great company.

Maybe they don’t start as a “computer programmer” there, maybe they start as an analyst or some other role.  But the
ability to program on top of that role will be very valuable and likely set up a great career.

## Conclusion

So I still think pursuing computer programming as a career is a good idea.  The current job market is bad, no doubt, but
I think this is temporary.

I do think how computer programming is done is changing, and programmers should look at building up skills beyond
“pure” code-writing.  This has always been a good idea.

I don’t think programming is changing as dramatically as some people claim and I think the fundamentals of programming,
particularly writing good code and controlling complexity, will be perennially important.

I hope this essay is useful in answering that question, especially for junior programmers, and helps people feel
more confident entering a career that I have found very rewarding and expect to continue to do for a long time.

And companies: let the juniors write at least some of the code.  It is in your interest.
</Release>

<Release date="January 16, 2026" published="2026-01-16T00:00:00.000Z" url="https://htmx.org/essays/paris-2024-olympics-htmx-network-automation/">
## Building Critical Infrastructure with htmx: Network Automation for the Paris 2024 Olympics

## A Bit of Background

During my 6 years at Cisco, I developed numerous web applications to assist network engineers with highly complex
operations, both in terms of the volume of tasks to accomplish and the rigor of procedures to follow. Networking is a
specialized field in its own right, where the slightest error can have disastrous consequences: a network failure, even
partial, can deprive millions of people of essential services like the ability to make a simple phone call.

This criticality imposes strict requirements on code meant for network operations: it must be reliable, readable, and
free of unnecessary frills. If there’s a problem, you need to be able to immediately trace the data flow and fix it on
the spot. That’s why, for years, I’ve used very few design patterns and banned function calls that call functions that
call functions, and so on. Beyond 2 levels of calls, I abstain.

Following this logic, I favor mature tools over the latest trends. Thus, the Django / Celery / SQLite stack had been in
my toolbox for a long time. But like everyone else in the 2010s, I built SPAs and had never heard of intercooler.js or
hypermedia, and I understood REST the way it’s commonly described pretty much everywhere.

For the JS framework, I made a conservative choice (and a marginal one, I know): I chose Ember.js. My motivations were
its strong backward compatibility during updates and native MVC support. This JS framework is excellent, and that’s
still my opinion.

After watching David Guillot’s presentation on HTMX at DjangoCon Europe 2022, I dug into the subject and prototyped a
component that addressed one of my recurring needs. It’s a kind of datatable on which you can trigger actions. There’s a
demo video on the HTMX Discord [here](https://discord.com/channels/725789699527933952/909436816388669530/1042451443656966234).

I was a beginner with HTMX and built it in 2-3 days (no AI :-) ). But what was interesting wasn’t so much having this
component quickly at hand : it was the 100% Django codebase. One codebase instead of two, one app to maintain, and no
more API contracts to manage between front and back.

And once again, even though I was comfortable with the Ember.js framework, having a single project to maintain changes
everything.

A few weeks later, a concrete use case came up for a major French ISP: configuring L2VPNs on brand-new routers, in bulk,
without configuration errors (obviously), based on configurations from old routers that were end-of-life and about to be
decommissioned. It was highly critical: a single router can handle thousands of clients, and… there are a lot of
routers.

From that point on, I used the Django / Celery + HTMX + SQLite (and Hyperscript) stack and delivered the app in 5 weeks.
My goal was to guide the network engineer by the hand and spare them 100% of the repetitive, tedious work: they just had
to click and confirm, everything was handled. Their role was now limited to their expertise, and if there was a problem,
it was up to them to fix the network.

The project, initially estimated at 18 months, ultimately took 9. And we were lucky: there were no complex corner cases
to handle. And even if there had been, we had plenty of time to deal with them.

HTMX in all this?
If I had to develop the app as a SPA, it would have taken me at least twice as long. Why?
As a solo full-stack developer, simply switching back and forth between codebases is already time-consuming. And that’s
just the tip of the iceberg: the front/back approach itself adds a layer of complexity that ends up weighing heavily on
productivity.

## HTMX at the Olympic Games

The Paris 2024 Olympic Games network consisted of thousands of Cisco switches pre-configured to accept Wi-Fi access
points, which self-configured through an automation system developed by Orange and Cisco. Wi-Fi was the most common
connectivity mode at the Games. But in some cases, a physical connection was necessary, most often to plug in a video
camera, but not only. Sometimes there was simply no other choice but to rely on a cable to connect, and therefore to
configure the relevant switch port. That’s where an application became necessary.

When Pollux contacted me about his need, he already had a data model for his network services in a Django project.
Additionally, he could deploy services via CLI: part of the business logic was already in place. The problem was that
service deployment parameters needed to be consolidated in an application. In CLI, you have to manage different data
sources, which can quickly become complicated for the user. So it was necessary to centralize these business parameters
in a webapp, expose all the data needed to deploy a service, and provide a GUI to configure them.

The Games were approaching and Pollux didn’t have time to build the webapp: as the architect of the Olympic network, he
was overwhelmed by a colossal number of tasks. I showed him the L2VPN app mentioned above and specified the 5-week
delivery timeline. I told him that if it suited him, I could build him an HTMX webapp based on his existing Django
project and a Bootstrap CSS customized internally by Orange.

We agreed on an 8-week timeline to cover the need, which involved 3 connectivity services: Direct Internet Access,
Private VLAN, and Shared Internet Access.

## Web Dev with HTMX

HTMX is somewhat a return to the roots of web development, and regardless of the web framework: Django, ROR, Symfony…
You rediscover everything that makes a web framework useful rather than turning it into a mere JSON provider. Sending
HTML directly to the browser, storing the app state directly in the HTML. That’s what true REST is, and it’s so much
simpler to manage.

If you ask me what’s most striking, it’s certainly returning to very simple things like this:

![progress bar](/img/paris-olympics-progress-bar.png)

 Progress bar from RCP Portal 

How does this progress bar work?

Exactly like [the example in the docs](https://htmx.org/examples/progress-bar/)!

Why this choice? Because it’s coded in 10 seconds, because the app won’t have thousands of users on this internal tool,
no scaling concerns: you can do good old data polling without any problem.

And the end user? If I use old-school polling, they don’t care: what they want is the information. No SSE or WebSocket
for this use case, I don’t need it. And if the need ever arises, the WebSocket (or SSE) plugin is easy to set up.

One of the big advantages of the philosophy surrounding HTMX is the notion of [Locality of Behaviour](https://htmx.org/essays/locality-of-behaviour/). Let’s take this
progress bar: if you want to know how it works, just look at the page source. No need to go into documentation or the
codebase, just a right-click and “View Page Source”:

```

div
        hx-get="/job/progress"
        hx-trigger="every 600ms"
        hx-target="this"
        hx-swap="innerHTML">
    div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
         aria-labelledby="pblabel">
        div id="pb" class="progress-bar" style="width:0%">
        div>
    div>

```

You immediately know that every 600ms, this part of the page is updated with the content returned by the view handling
the `/job/progress` endpoint. No mystery for the team taking over development who wants to modify something: everything
you need to know is right in front of you.

And that’s exactly what HTMX is about: every component, every interaction remains visible, understandable, and
self-documented directly in the HTML. This is important for what comes next.

## HTMX is “AI friendly”™

In the early stages of app development, I focused on the most complex network service: DIA (Direct Internet Access). DIA
for the Olympic Games meant many business parameters with many rules to apply.

The DIA creation form calls an endpoint that triggers a very long function, close to 600 lines of code.

Why such a long function?

Because it’s more readable and efficient to concentrate the data flow in one place, rather than dispersing it across a
multitude of layers and patterns.

An application is a wrapper around data: it orchestrates the data flow (data must flow!) and CRUD operations. But what
control do you retain when this flow is obfuscated in complex patterns or dispersed across 2 codebases?

The data flow must remain readable for the developer.

HTMX, by allowing you to manage the GUI directly server-side, makes this flow even clearer. The same endpoint can return
HTML fragments to signal that certain form data is invalid, or conversely indicate that a service deployment has
started. You can thus act on any part of the GUI within the same function, while transforming the data to pass it to the
system that configures the network switch.

In a traditional frontend/backend approach, this would be more complex: two applications to manage, and a much less
readable data flow.

This drastic code simplification enabled by HTMX, combined with a procedural approach, produces compact and transparent
logic, easy to navigate for a developer… or for an LLM, as I discovered.

For the Private VLAN (PVLAN) network service, the “shape” of the main function is roughly the same as for DIA: input
parameters, validation, then interactions with the GUI via HTML fragments, and, if everything is correct, switch
configuration.

The difference? PVLAN is simpler to handle: fewer form parameters and a bit less business logic.

So I took the long DIA function and gave it to an LLM (Claude 3 had just been released), with a prompt specifying the
parameters specific to PVLAN. In seconds, Claude returned a new adapted function, and the same for the HTML templates.
Result: about 80% of the code was ready, with only a few points to correct and relatively few errors made by the LLM,
which freed up time for me to add specific business logic for a major client.

For the third network service, Shared Internet Access (SIA), even simpler than the previous two, I provided the LLM with
both the DIA and PVLAN functions. With the magic word *“extrapolation”* in the prompt, the generated code was 95%
correct.

## Summary of My Experience

- **DIA**: 0% AI, 100% handwritten code (business logic + GUI + overhaul of the switch configuration task management
system) → **4 weeks**

- **PVLAN**: 80% AI, 20% handwritten code (corrections + adding specific business logic) → **1 week**

- **SIA**: 95% AI, 5% handwritten code (minor corrections) → **1 day**

The time saved was reinvested in testing, bug fixes, project management, and even a few additions outside the initial
scope.

Moreover, the same app was used on the “Tour de France 2025” with minor changes that were made easily thanks to the
hypermedia approach.

This result is possible because of the combination of *HTMX + the procedural approach*, which produces naturally
readable code, without unnecessary abstraction layers. The data flow is clear, concentrated in a single function, and
the GUI/server logic is directly accessible.

For an LLM, this is ideal: it doesn’t need to construct context through a complex architecture. It just needs to follow
the flow and extrapolate it to a new use case. In other words, what’s simpler for the developer is also simpler for the
AI. This is the sense in which HTMX is truly *“AI friendly”™*.

Ultimately, HTMX mainly allowed me to save time and keep my code clear.
No unnecessary layers, no superfluous complexity: just concrete stuff that works, fast.

And that has made a big difference on these critical projects.
</Release>

<Release date="November 1, 2025" published="2025-11-01T00:00:00.000Z" url="https://htmx.org/essays/the-fetchening/">
## The fetch()ening

![Stop trying to make fetch() happen](/img/fetch.png)

OK, I said there would never be a version three of htmx.

But, *technically*, I never said anything about a version *four*…

## htmx 4: The fetch()ening

In [The Future of htmx](https://htmx.org/essays/hypermedia-driven-applications/) I said the following:

We are going to work to ensure that htmx is extremely stable in both API & implementation. This means accepting and
documenting the [quirks](https://htmx.org/quirks/) of the current implementation.

Earlier this year, on a whim, I created [fixi.js](https://github.com/bigskysoftware/fixi), a hyperminimalist implementation
of the ideas in htmx.  That work gave me a chance to get a lot more familiar  with the `fetch()` and, especially, the
[async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) infrastructure
available in JavaScript.

In doing that work I began to wonder if that, while the htmx [API](https://htmx.org/reference/#attributes)
is (at least reasonably) correct, maybe there was room for a more dramatic change of the implementation that took
advantage of these features in order to simplify the library.

Further, changing from ye olde [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)
(a legacy of htmx 1.0 IE support) to [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) would
be a pretty violent change, guaranteed to break at least *some* stuff.

So I began thinking: if we are going to consider moving to fetch, then maybe we should *also* use this update as a
chance address at least *some* of the [quirks & cruft](https://htmx.org/quirks/) that htmx has acquired over its lifetime.

So, eventually & reluctantly, I have changed my mind: there *will* be another major version of htmx.

However, in order to keep my word that there will not be a htmx 3.0, the next release will instead be htmx 4.0.

## Project Goals

With htmx 4.0 we are rebuilding the internals of htmx, based on the lessons learned from
fixi.js and [five+ years](https://www.npmjs.com/package/htmx.org/v/0.0.1) of supporting htmx.

There are three major simplifying changes:

### The fetch()ening

The biggest internal change is that `fetch()` will replace `XMLHttpRequest` as the core ajax infrastructure.  This
won’t actually have a huge effect on most usages of htmx *except* that the events model will necessarily change due
to the differences between `fetch()` and `XMLHttpRequest`.

### Explicit Inheritance By Default

I feel that the biggest mistake in htmx 1.0 & 2.0 was making attribute inheritance implicit.  I was inspired by CSS in
doing this, and the results have been roughly the same as CSS: powerful & maddening.

In htmx 4.0, attribute inheritance will be explicit by default rather than implicit.  Explicit inheritance will
be done via the `:inherited` modifier:

```
  div hx-target:inherited="#output">
    button hx-post="/up">Likebutton>
    button hx-post="/down">Dislikebutton>
  div>
  output id="output">Pick a button...output>

```

Here the `hx-target` attribute is explicitly declared as `inherited` on the enclosing `div` and, if it wasn’t, the
`button` elements would not inherit the target from it.

You will be able to revert to htmx 2.0 implicit inheritance behavior via a configuration variable.

### No Locally Cached History

Another source of pain for both us and for htmx users is history support.  htmx 2.0 stores history in local
cache to make navigation faster.  Unfortunately, snapshotting the DOM is often brittle because of third-party
modifications, hidden state, etc.  There is a terrible simplicity to the web 1.0 model of blowing everything away and
starting over.  There are also security concerns storing history information in session storage.

In htmx 2.0, we often end up recommending that people facing history-related issues simply disable the cache entirely,
and that usually fixes the problems.

In htmx 4.0, history support will no longer snapshot the DOM and keep it locally.  It will, rather, issue a network
request for the restored content.  This is the behavior of 2.0 on a history cache-miss, and it works reliably with
little effort on behalf of htmx users.

We will offer an extension that enables history caching like in htmx 2.0, but it will be opt-in, rather than the default.

This tremendously simplifies the htmx codebase and should make the out-of-the-box behavior much more plug-and-play.

## What Stays The Same?

Most things.

The [core](https://dl.acm.org/doi/10.1145/3648188.3675127) functionality of htmx will remain the same, `hx-get`, `hx-post`,
`hx-target`, `hx-boost`, `hx-swap`, `hx-trigger`, etc.

With a few configuration tweaks, most htmx 2.x based applications should work with htmx 4.x.

These changes will make the long term maintenance & sustainability of the project much stronger.  It will also take
pressure off the 2.0 releases, which can now focus on stability rather than contemplating new features.

## Upgrading

htmx 2.0 users *will* face an upgrade project when moving to 4.0 in a way that they did not have to in moving
from 1.0 to 2.0.

I am sorry about that, and want to offer two things to address it:

- htmx 2.0 (like htmx 1.0 & intercooler.js 1.0) will be supported *in perpetuity*, so there is absolutely *no* pressure to
upgrade your application: if htmx 2.0 is satisfying your hypermedia needs, you can stick with it.

- We will roll htmx 4.0 out slowly, over a multi-year period.  As with the htmx 1.0 -> 2.0 upgrade, there will be a long
period where htmx 2.x is `latest` and htmx 4.x is `next`

## New Features

Beyond simplifying the implementation of htmx significantly, switching to fetch also gives us the opportunity to add
some nice new features to htmx

### Streaming Responses & SSE in Core

By switching to `fetch()`, we can take advantage of its support for
[readable streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams), which
allow for a stream of content to be swapped into the DOM, rather than a single response.

htmx 1.0 had Server Sent Event support integrated into the library.  In htmx 2.0 we pulled this functionality out as an
extension.  It turns out that SSE is just a specialized version of a streaming response, so in adding streaming
support, it’s an almost-free free two-fer to add that back into core as well.

This will make incremental response swapping much cleaner and well-supported in htmx.

### Morphing Swap in Core

[Three years ago](https://github.com/bigskysoftware/idiomorph/commit/7760e89d9f198b43aa7d39cc4f940f606771f47b) I had
an idea for a DOM morphing algorithm that improved on the initial algorithm pioneered by [morphdom](https://github.com/patrick-steele-idem/morphdom).

The idea was to use “id sets” to make smarter decisions regarding which nodes to preserve and which nodes to delete when
merging changes into the DOM, and I called this idea “idiomorph”.  Idiomorph has gone on to be adopted by many other
web project such as [Hotwire](https://hotwired.dev/).

We strongly considered including it in htmx 2.0, but I decided not too because it worked well as an extension and
htmx 2.0 had already grown larger than I wanted.

In 4.0, with the complexity savings we achieved by moving to `fetch()`, we can now comfortably fit a `morphInner` and
`morphOuter` swap into core, thanks to the excellent work of Michael West.

### Explicit  Tag Support

htmx has, since very early on, supported a concept of “Out-of-band” swaps: content that is removed from the main HTML
response and swapped into the DOM elsewhere.  I have always been a bit ambivalent about them, because they move away
from [Locality of Behavior](https://htmx.org/essays/locality-of-behaviour/), but there is no doubt that they are useful
and often crucial for achieving certain UI patterns.

Out-of-band swaps started off very simply: if you marked an element as `hx-swap-oob='true'`, htmx would swap the element
as the outer HTML of any existing element already in the DOM with that id.  Easy-peasy.

However, over time, people started asking for different functionality around Out-of-band swaps: prepending, appending,
etc. and the feature began acquiring some fairly baroque syntax to handle all these needs.

We have come to the conclusion that the problem is that there are really *two* use cases, both currently trying to be
filled by Out-of-band swaps:

- A simple, id-based replacement

- A more elaborate swap of partial content

Therefore, we are introducing the notion of ``s in htmx 4.0

A partial element is, under the covers, a template element and, thus, can contain any sort of content you like.  It
specifies on itself all the standard htmx options regarding swapping, `hx-target` and `hx-swap` in particular, allowing
you full access to all the standard swapping behavior of htmx without using a specialized syntax.  This tremendously
simplifies the mental model for these sorts of needs, and dovetails well with the streaming support we intend to offer.

Out-of-band swaps will be retained in htmx 4.0, but will go back to their initial, simple focus of simply replacing
an existing element by id.

### Improved View Transitions Support

htmx 2.0 has had [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) support since
[April of 2023](https://github.com/bigskysoftware/htmx/blob/master/CHANGELOG.md#190---2023-04-11).  In the interceding
two years, support for the feature has grown across browsers (c’mon, safari, you can do it) and we’ve gained experience
with the feature.

One thing that has become apparent to us while using them is that, to use them in a stable manner, it is important
to establish a *queue* of transitions, so each can complete before the other begins.  If you don’t do this, you can get
visually ugly transition cancellations.

So, in htmx 4.0 we have added this queue which will ensure that all view transitions complete smoothly.

CSS transitions will continue to work as before as well, although the swapping model is again made much simpler by the
async runtime.

We may enable View Transitions by default, the jury is still out on that.

### Stabilized Event Ordering

A wonderful thing about `fetch()` and the async support in general is that it is *much* easier to guarantee a stable
order of events.  By linearizing asynchronous code and allowing us to use standard language features like try/catch,
the event model of htmx should be much more predictable and comprehensible.

We are going to adopt a new standard for event naming to make things even clearer:

`htmx::[:]`

So, for example, `htmx:before:request` will be triggered before a request is made.

### Improved Extension Support

Another opportunity we have is to take advantage of the `async` behavior of `fetch()` for much better performance in our
preload extension (where we issue a speculative (`GET` only!) request in anticipation of an actual trigger).  We have
also added an optimistic update extension to the core extensions, again made easy by the new async features.

In general, we have opened up the internals of the htmx request/response/swap cycle much more fully to extension developers,
up to and including allowing them to replace the `fetch()` implementation used by htmx for a particular request.  There
should not be a need for any hacks to get the behavior you want out of htmx now: the events and the open “context” object
should provide the ability to do almost anything.

### Improved `hx-on` Support

In htmx 2.0, I somewhat reluctantly added the [`hx-on`](https://htmx.org/attributes/hx-on/) attributes to support light
scripting inline on elements.  I added this because HTML does not allow you to listen for arbitrary events via `on`
attributes: only standard DOM events like `onclick` can be responded to.

We hemmed and hawed about the syntax and so, unfortunately, there are a few different ways to do it.

In htmx 4.0 we will adopt a single standard for the `hx-on` attributes: `hx-on:`.  Additionally, we are
working to improve the htmx JavaScript API (especially around async operation support) and will make those features
available in `hx-on`:

```
button hx-post="/like"
        hx-on:htmx:after:swap="await timeout('3s'); ctx.newContent[0].remove()">
    Get A Response Then Remove It 3 Seconds Later
button>

```

htmx will never support a fully featured scripting mechanism in core, we recommend something like
[Alpine.js](https://alpinejs.dev/) for that, but our hope is that we can provide a relatively minimalist API that
allows for easy, light async scripting of the DOM.

I should note that htmx 4.0 will continue to work with `eval()` disabled, but you will need to forego a few features like
`hx-on` if you choose to do so.

### A Better But Familiar htmx

All in all, our hope is that htmx 4.0 will feel an awful lot like 2.0, but with better features and, we hope, with fewer bugs.

## Timeline

As always, software takes as long as it takes.

However, our current planned timeline is:

- An alpha release is available *today*:  `htmx@4.0.0-alpha1`

- A 4.0.0 release should be available in early-to-mid 2026

- 4.0 will be marked `latest` in early-2027ish

You can track our progress (and see quite a bit of dust flying around) in the `four` branch on
[github](https://github.com/bigskysoftware/htmx/tree/four) and at:

[https://four.htmx.org](https://four.htmx.org)

Thank you for your patience and pardon our dust!

“Well, when events change, I change my mind. What do you do?” –Paul Samuelson or John Maynard Keynes
</Release>

<Release date="February 19, 2025" published="2025-02-19T00:00:00.000Z" url="https://htmx.org/essays/interviews/leonard-richardson/">
## An interview with Leonard Richardson

Leonard Richardson is a long time programmer and author and was the creator of what came to be termed the Richardson
Maturity
Model ([https://en.wikipedia.org/wiki/Richardson_Maturity_Model](https://en.wikipedia.org/wiki/Richardson_Maturity_Model)),
a system for classifying Web APIs in terms of their adherence to REST. Here, Web APIs mean *data APIs*, that is data
intended to be consumed by automated systems or code, rather than directly by a web client.

The RMM consists of four levels:

- Level 0 - The Swamp of POX (Plain old XML)

- Level 1 - The appropriate use of resource-based URLs

- Level 2 - The appropriate use of HTTP Methods

- Level 3 - Hypermedia Controls

A Web API became more mature as it adopted these technologies and conventions.

Leonard agreed to talk to me about the RMM and his experiences building Web software.

**Question**: Can you give us some background about yourself and how you came into web programming? Did/do you consider
yourself a hypermedia enthusiast?

When I was in high school in the mid-1990s, I got very basic Internet access through BBSes. There were all these
amazing, arcane protocols you had to learn to get around: FTP, Gopher, Archie, NNTP, et cetera. And then just as I went
to college, the World Wide Web suddenly wiped out all of those domain-specific protocols. Within a couple of years we
were using the Web technologies–URI, HTTP and HTML–for everything.

My formative years as a developer happened against the background of the incredible power of those three core
technologies. Everything was being built on top of the Web, and it basically worked and was a lot simpler than the old
way.

So yes, I am a hypermedia enthusiast, but it took me a really long time to understand the distinct advantages that come
from the “hypermedia” part of the Web. And that came with an understanding of when those advantages are irrelevant or
undesirable from a business standpoint.

**Question**: Can you give us a brief history of early Web APIs? What was the origin story of the RMM?

What we now call “web APIs” started as a reaction against SOAP, a technology from a time when corporate firewalls
allowed HTTP connections (necessary to get work done) but blocked most other traffic (games?). SOAP let you serialize a
procedure call into XML and invoke it over an HTTP connection, punching through the firewall. It was an extraordinarily
heavyweight solution, using the exact same tools–XML and HTTP–you’d need to make a good lightweight solution.

Now that I’m an old fogey, I can look back on SOAP and see the previous generation of old fogeys trying to make the
1990s client-server paradigm work over the Internet. SOAP had a lot of mindshare for a while, but there were very few
publicly deployed SOAP services. When you deploy a service on the public Internet, people expect to connect to it from a
wide variety of programming languages and programming environments. SOAP wasn’t cut out for that because it was so heavy
and demanded so much tooling to compensate for its heaviness.

Instead, developers picked and chose their designs from the core web technologies. Thanks to the dot-com boom, those
technologies were understood by practicing developers and well supported by every major programming language.

The RMM, as it’s now called, was originally a heuristic I presented
in [a talk in 2008](https://www.crummy.com/writing/speaking/2008-QCon/act3.html). [The first part of the talk](https://www.crummy.com/writing/speaking/2008-QCon/act1.html)
goes over the early history I mentioned earlier,
and [the second part](https://www.crummy.com/writing/speaking/2008-QCon/act2.html) talks about my first experience
trying to sell hypermedia-based API design to an employer.

I’d analyzed about a hundred web API designs for my book on REST and seen very strong groupings around the core web
technologies. You’d see a lot of APIs that “got” URLs but didn’t “get” HTTP. But you’d never see one where it happened
the other way, an API that took advantage of the features of HTTP but didn’t know what to do with URLs. If I had one
insight here, it’s that the URL is the most fundamental web technology. HTTP is a set of rules for efficiently dealing
with URLs, and HTML (a.k.a. hypermedia) is a set of instructions for driving an HTTP client.

**Question**: In “How Did REST come to mean the opposite of REST?” I assert that the term REST has nearly inverted in
its meaning. In particular, I claim that most APIs stopped at “Level 2” of the RMM. Do you agree with these claims?

Everyone understands URIs these days, and understanding HTTP is essential for performance reasons if nothing else. That
gets you to level 2, and yes, there we have stayed. That’s what I was getting at
in [this interview from 2007](https://www.infoq.com/articles/richardson-ruby-restful-ws/), a year before I gave my
famous talk:

The big question in my mind is whether architectures consciously designed with REST in mind will “win” over
architectures that are simple but only intermittently RESTful.

You don’t get a certificate signed by Roy Fielding for achieving level 3. The reward for learning and applying the
lesson of hypermedia is *interoperability*. Your users get the ability to use your system in ways you didn’t anticipate,
and they get to combine your system with their other systems.

Interoperability is essential in a situation like the Web, where there are millions of server and client deployments,
and a dozen major server implementations. (There are now only two major client implementations, but that’s its own sad
story.)

For a long time I thought people just didn’t get this and if I hammered on the technical advantages of hypermedia they’d
come around. But we’ve been stuck at level 2 for more than half the lifetime of the Web. It’s become clear to me that
most situations aren’t like the Web, and the advantages of hypermedia aren’t relevant to most business models.

**Question**: Level 3 style hypermedia controls never really took off in Web APIs. Why do you think that is?

I don’t do this for everything, but I am going to blame this one on capitalism.

Almost all actually deployed web APIs are under the complete control of one company. They have one server implementation
written by that company and one server deployment managed by that company. If the API has any official client
implementations, those are also controlled by the company that owns the API. The fact that we say “the [company] API”
is the opposite of interoperability.

Users like interoperability, but vendors prefer lock-in. We see that in their behavior. Netflix was happy to provide a
hypermedia API for their program data… until their streaming business became big enough. Once they were the dominant
player and could dictate the terms of integration, Netflix shut down their API. Twitter used to cut off your API access
if your client got too popular; then they banned third-party clients altogether.

There are lots of APIs that consider interoperability a strong point, and many of them are oriented around hypermedia,
but almost all of them live outside the space of commercial competition. In 2008 when I gave the “maturity heuristic”
talk I was working on software development tools at Canonical, which is a for-profit company but heavily involved in
open source development. We wanted lots of tools and programming environments to be able to talk to our API, but the API
was a relatively small part of our process and we didn’t have enough developers to manage a bunch of official clients. A
hypermedia-based approach gave us a certain flexibility to change our API without breaking all the clients.

After that I spent eight years working on ebook delivery in the US public library space, which is extremely fragmented
in terms of IT management. In a nonprofit environment with lots of independent server deployments, hypermedia (in the
form of the [OPDS](https://opds.io/) protocol) was a really easy
pitch. [I gave a talk about that.](https://www.crummy.com/writing/speaking/2015-RESTFest/)

To get the benefits of hypermedia you have to collaborate with other people in the same field, consider the entire
problem space, and come up with a design that works for everyone. Who’s going to go through all that work when the
reward is “no vendor lock-in”? People who are not competing with their peers: scientists, librarians, and open source
developers.

It might or might not surprise you to learn that the library world is dominated by an antique protocol
called [SIP](https://developers.exlibrisgroup.com/wp-content/uploads/2020/01/3M-Standard-Interchange-Protocol-Version-2.00.pdf). (
Not the VoIP protocol, a different SIP.) SIP is what the self-checkout machine uses to record the fact that you borrowed
the book. SIP first showed up in 1993, its design is distinctively non-RESTful, and in many ways it’s simply *bad*.
Every library vendor has come up with their own level 2 “REST” protocol to do what SIP does. But none have succeeded in
displacing SIP, because that awful old 1993 design provides something a vendor can’t offer: interoperability between
components from different
vendors. [I gave a talk about that, too.](https://www.crummy.com/writing/speaking/2016-RESTFest/)

**Question**: Do you think the move from XML to JSON as a format had any influence on how Web APIs evolved?

Absolutely. Moving from XML to JSON replaced a document-centric design (more suitable for communications with a human at
one end) with a data-centric design (more suitable for machine-to-machine communication). The cost was forgetting about
hypermedia altogether.

One thing about Martin’s diagram that I think obscures more than it reveals is: he calls level 0 the “Swamp of POX”.
This makes it seem like the problem is (Plain Old) XML. Martin is actually talking about SOAP there. The big problem
with SOAP services isn’t XML (although they do have way too much XML), it’s that they don’t use URLs. A SOAP client puts
all of the request information into an XML package and tunnels it through a single service endpoint. This makes SOAP
opaque to the tools designed to manage and monitor and inspect HTTP traffic. This is by design, because the point of
SOAP is to let you make RPC calls when your IT department has everything but port 80 locked down.

Anyway, XML is great! It’s too verbose to make an efficient data representation format, but XML has namespaces, and
through namespaces it has hypermedia controls (via XLink, XForms, XHTML, Atom, etc.). JSON has no hypermedia controls,
and because it also has no namespaces, you can’t add them after the fact.

People started adopting JSON because they were tired of XML processing and excited about AJAX (in-browser HTTP clients
driven by Javascript, for those who weren’t there). But that initial decision started constraining decisions down the
road.

By 2011, all new web APIs were using a representation format with no hypermedia controls. You couldn’t do a
hypermedia-based design if you wanted to. Our technical language had lost the words. First you’d have to define a
JSON-based media type that had hypermedia (like Siren), or namespaces (like JSON-LD).

**Question**: What are your thoughts on GraphQL and other non-RESTful API technologies?

With regard to non-RESTful API technologies in general, I would suggest that folks take a break
from [chapter 5](https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) of Roy Fielding’s dissertation,
and look at chapters [2](https://ics.uci.edu/~fielding/pubs/dissertation/net_app_arch.htm%20)
and [4](https://ics.uci.edu/~fielding/pubs/dissertation/web_arch_domain.htm).

Chapter 5 is where Fielding talks about the design of the Web, but Chapter 2 breaks down all of the possible good things
you might want from a networked application architecture, only some of which apply to the Web. Chapter 4 explains the
tradeoffs that were made when designing the Web, giving us some good things at the expense of others.

Chapter 4 lists five main advantages of the Web: low entry-barrier, extensibility, distributed hypermedia, anarchic
scalability, and independent deployment. REST is a really effective way of getting those advantages, but the advantages
themselves are what you really want. If you can get them without the Web technologies, then all you’ve lost is the
accumulated expertise that comes with those technologies (although that ain’t nothing at this point). And if you *don’t*
want some of these advantages (probably distributed hypermedia) you can go back to chapter 2, start the process over,
and end up with a differently optimized architecture.

I don’t have any direct experience with GraphQL, though I’m about to get some at my current job, so take this with a
grain of salt:

On a technical level, GraphQL is solving a problem that’s very common in API design: performing a database query across
a network connection without sending a bunch of unneeded data over the wire. Looking at the docs I see it also has “
Mutations“ which seem very SOAP-ish. I guess I’d say GraphQL looks like a modern version of SOAP, optimized for the
common case of querying a database.

Since GraphQL is independently deployable, supports multiple server implementations and defines no domain-specific
semantics, an interoperable domain-specific API could be built on top of it. Rather than exporting your data model to
GraphQL and clashing with a dozen similar data models from the same industry, you could get together with your peers and
agree upon a common set of semantics and mutations for your problem space. Then you’d have interoperability. It’s not
much different from what we did with OPDS in the library world, defining what concepts like “bookshelf” and “borrow”
mean.

Would it be RESTful? Nope! But again I’ll come back to SIP, the integration protocol that public libraries use to keep
track of loans. SIP is a level zero protocol! It doesn’t use any of the Web technologies at all! But it provides
architectural properties that libraries value and vendor-centric solutions can’t offer–mainly interoperability–so it
sticks around despite the presence of “RESTful” solutions.
</Release>

<Release date="January 27, 2025" published="2025-01-27T00:00:00.000Z" url="https://htmx.org/essays/vendoring/">
## Vendoring

“Vendoring” software is a technique where you copy the source of another project directly into your own project.

It is an old technique that has been used for time immemorial in software development, but the term “vendoring” to
describe it appears to have originated in the [ruby community](https://stackoverflow.com/posts/72115282/revisions).

Vendoring can be and is still used today. You can vendor htmx, for example, quite easily.

Assuming you have a `/js/vendor` directory in your project, you can just download the source into your own project like
so:

```
curl https://raw.githubusercontent.com/bigskysoftware/htmx/refs/tags/v2.0.4/dist/htmx.min.js > /js/vendor/htmx-2.0.4.min.js

```

You then include the library in your `head` tag:

```
script src="/js/vendor/htmx-2.0.4.min.js">script>

```

And then you check the htmx source into your own source control repository.  (I would even recommend considering using
the [non-minimized version](https://raw.githubusercontent.com/bigskysoftware/htmx/refs/tags/v2.0.4/dist/htmx.js), so
you can better understand and debug the code.)

That’s it, that’s vendoring.

## Vendoring Strengths

OK, great, so what are some strengths of vendoring libraries like this?

It turns out there are quite a few:

- Your entire project is checked in to your source repository, so no external systems beyond your source control need
to be involved when building it

- Vendoring dramatically improves dependency *visibility*: you can *see* all the code your project depends on, so you
won’t have a situation like we have in htmx, where we feel like we only have a few development dependencies, when in
fact we may have a lot

- This also means if you have a good debugger, you can step into the library code as easily as any other code.  You
can also read it, learn from it and even modify it if necessary.

- From a security perspective, you aren’t relying on opaque code.  Even if your package manager has
an integrity hash system, the actual code may be opaque to you.  With vendored code it is checked in and can be
analysed automatically or by a security team.

- Personally, it has always seemed crazy to me that people will often resolve dependencies at deployment time, right
when your software is about to go out the door.  If that bothers you, like it does me, vendoring puts a stop to it.

On the other hand, vendoring also has one massive drawback: there typically isn’t a good way to deal with what is called
the [transitive dependency](https://en.wikipedia.org/wiki/Transitive_closure) problem.

If htmx had sub-dependencies, that is, other libraries that it depended on, then to vendor it properly you would have to
start vendoring all those libraries as well.  And if those dependencies had further dependencies, you’d need to install
them as well… And on and on.

Worse, two dependencies might depend on the same library, and you’ll need to make sure you get the
[correct version](https://en.wikipedia.org/wiki/Dependency_hell) of that library for everything to work.

This can get pretty difficult to deal with, but I want to make a paradoxical claim that this weakness (and, again, it’s
a real one) is actually a strength in some way:

Because dealing with large numbers of dependencies is difficult, vendoring encourages a culture of *independence*.

You get more of what you make easy, and if you make dependencies easy, you get more of them.  Making dependencies,
*especially* transitive dependencies, more difficult would make them less common.

And, as we will see in a bit, maybe fewer dependencies isn’t such a bad thing.

## Dependency Managers

That’s great and all, but there are [significant](https://gist.github.com/datagrok/8577287)
[drawbacks](https://web.archive.org/web/20180216205752/http://blog.bithound.io/why-we-stopped-vendoring-our-npm-dependencies/)
to vendoring, particular the transitive dependency problem.

Modern software engineering uses dependency managers to deal with the dependencies of software projects.  These tools
allow you to specify your projects dependencies, typically via some sort of file.  They then they will install those
dependencies and resolve and manage all the other dependencies that are necessary for those dependencies to work.

One of the most widely used package managers is NPM: The [Node Package Manager](https://www.npmjs.com/).  Despite having
no runtime dependencies, htmx uses NPM to specify 16 development dependencies.  Development dependencies are dependencies
that are necessary for development of htmx, but not for running it.  You can see the dependencies at the bottom of
the NPM [`package.json`](https://github.com/bigskysoftware/htmx/blob/master/package.json) file for the project.

Dependency managers are a crucial part of modern software development and many developers today couldn’t imagine
writing software without them.

### The Trouble with Dependency Managers

So dependency managers solve the transitive dependency problem that vendoring has.  But, as with everything in software
engineering, there are tradeoffs associated with them.  To see some of these tradeoffs, let’s take a look at the
[`package-lock.json`](https://github.com/bigskysoftware/htmx/blob/master/package-lock.json) file in htmx.

NPM generates a `package-lock.json` file that contains the resolved transitive closure of dependencies for a project, with
the concrete versions of those dependencies.  This helps ensure that the same dependencies are used unless a user
explicitly updates them.

If you take a look at the `package-lock.json` for htmx, you will find that the original 13 development dependencies have
ballooned into a total of 411 dependencies when all is said and done.

htmx, it turns out, relies on a huge number of packages, despite priding itself on being a relatively lean.  In fact,
the `node_modules` folder in htmx is a whopping 110 megabytes!

But, beyond this bloat there are deeper problems lurking in that mass of dependencies.

While writing this essay I found that htmx apparently depends on the
[`array.prototype.findlastindex`](https://www.npmjs.com/package/array.prototype.findlastindex), a
[polyfill](https://en.wikipedia.org/wiki/Polyfill_(programming)) for a JavaScript feature introduced in
[2022](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex).

Now, [htmx 1.x](https://v1.htmx.org/) is IE compatible, and I don’t *want* polyfills for *anything*: I want to write
code that will work in IE without any additional library support.  And yet a polyfill has snuck in via a chain
of dependencies (htmx does not directly rely on it) that introduces a dangerous polyfill that would let me write
code that would break in IE, as well as other older browsers.

This polyfill may or may not be available when I run the htmx [test suite](https://htmx.org/test/) (it’s hard to tell)
but that’s the point: some dangerous code has snuck into my project without me even knowing it, due to the number
and complexity of the (development) dependencies it has.

This demonstrates significant *cultural* problem with dependency managers:

They tend to foster a culture of, well, dependency.

A spectacular example of this was the infamous [left-pad incident](https://en.wikipedia.org/wiki/Npm_left-pad_incident),
in which an engineer took down a widely used package and broke the build at companies like Facebook, PayPal, Netflix,
etc.

That was a relatively innocuous, although splashy, issue, but a more serious concern is
[supply chain attacks](https://en.wikipedia.org/wiki/Supply_chain_attack), where a hostile entity is able to compromise
a company via code injected unwittingly via dependencies.

The larger our dependency graph gets, the worse these problems get.

## Dependencies Reconsidered

I’m not the only person thinking about our culture of dependency.  Here’s what some other, smarter folks have to say
about it:

[Armin Ronacher](https://x.com/mitsuhiko), creator of [flask](https://flask.palletsprojects.com/en/stable/)
recently said this on [the ol’twits](https://x.com/mitsuhiko/status/1882739157120041156):

The more I build software, the more I despise dependencies. I greatly prefer people copy/pasting stuff into their own
code bases or re-implement it. Unfortunately the vibe of the time does not embrace that idea much. I need that vibe
shift.

He also wrote a great blog post about his
[experience with package management](https://lucumr.pocoo.org/2025/1/24/build-it-yourself/) in the Rust ecosystem:

It’s time to have a new perspective: we should give kudos to engineers who write a small function themselves instead
of hooking in a transitive web of crates. We should be suspicious of big crate graphs. Celebrated are the minimal
dependencies, the humble function that just quietly does the job, the code that doesn’t need to be touched for years
because it was done right once.

Please go read it in full.

Back in 2021, [Tom Macwright](https://macwright.com) wrote this in
[Vendor by default](https://macwright.com/2021/03/11/vendor-by-default)

But one thing that I do think is sort of unusual is: I’m vendoring a lot of stuff.

Vendoring, in the programming sense, means “copying the source code of another project into your project.” It’s in
contrast to the practice of using dependencies, which would be adding another project’s name to your package.json
file and having npm or yarn download and link it up for you.

I highly recommend reading his take on vendoring as well.

## Software Designed To Be Vendored

Some good news, if you are an open source developer and like the idea of vendoring, is that there is a simple way to
make your software vendor-friendly: remove as many dependencies as you can.

[DaisyUI](https://daisyui.com/), for example, has been in the process of
[removing their dependencies](https://x.com/Saadeghi/status/1882556881253826941), going from 100 dependencies in
version 3 to 0 in version 5.

There is also a set htmx-adjacent projects that are taking vendoring seriously:

- [Surreal](https://github.com/gnat/surreal) - a lightweight jQuery alternative

- [Facet](https://github.com/kgscialdone/facet) - an HTML-oriented Web Component library

- [fixi](https://github.com/bigskysoftware/fixi) - a minimal htmx alternative

None of these JavaScript projects are available in NPM, and all of them [recommend](https://github.com/gnat/surreal#-install)
[vendoring](https://github.com/kgscialdone/facet#installation) the [software](https://github.com/bigskysoftware/fixi#installing)
into your own project as the primary installation mechanism.

## Vendor First Dependency Managers?

The last thing I want to briefly mention is a technology that combines both vendoring and dependency management:
vendor-first dependency managers.  I have never worked with one before, but I have been pointed to
[vend](https://github.com/fosskers/vend), a common lisp vendor oriented package manager (with a great README), as well
as [go’s vendoring option](https://go.dev/ref/mod#vendoring).

In writing this essay, I also came across [vendorpull](https://github.com/sourcemeta/vendorpull) and
[git-vendor](https://github.com/brettlangdon/git-vendor), both of which are small but interesting projects.

These all look like excellent tools, and it seems to me that there is an opportunity for some of them (and tools like
them) to add additional functionality to address the traditional weaknesses of vendoring, for example:

- Managing transitive dependencies, if any

- Relatively easy updates of those dependencies

- Managing local modifications made to dependencies (and maybe help manage contributing them upstream?)

With these additional features I wonder if vendor-first dependency managers could compete with “normal” dependency
managers in modern software development, perhaps combining some of the benefits of both approaches.

Regardless, I hope that this essay has helped you think a bit more about dependencies and perhaps planted the idea that
maybe your software could be a little less, well, dependent on dependencies.
</Release>

<Release date="January 27, 2025" published="2025-01-27T00:00:00.000Z" url="https://htmx.org/essays/interviews/makinde-adeagbo/">
## An interview with Makinde Adeagbo, Creator of Primer

I’m delighted to be able to interview Makinde Adeagbo, one of the creators of [Primer](https://www.youtube.com/watch?v=wHlyLEPtL9o),
an hypermedia-oriented javascript library that was being used at Facebook in the 2000s.

Thank you for agreeing to an interview!

Q: To begin with, why don’t you give the readers a bit of your background both professionally & technically?

I’ve always been into tech. In high school, I used to build computers for friends and family. I took the computer science classes my high school offered and went on to study computer science in college. I was always amazed by the fact that I could build cool things—games, tools, etc.—with just a computer and an internet connection.

I was lucky enough to participate in Explore Microsoft, an internship that identifies underrepresented college freshmen and gives them a shot at working at Microsoft. After that experience, I was sold on software as my future. I later interned at Apple and Microsoft again. During college, I also worked at Facebook when the company was about 150 employees. It was an incredible experience where engineers had near-total freedom to build and contribute to the company’s growth. It was exactly what I needed early in my career, and I thrived. From there, I went on to work at Dropbox and Pinterest and also co-founded the nonprofit, /dev/color.

Q: Can you give me the history of how Primer came to be?

In 2010, the Facebook website was sloooow. This wasn’t the fault of any specific person—each engineer was adding features and, along the way, small amounts of JavaScript. However, we didn’t have a coherent system for sharing libraries or tracking how much JavaScript was being shipped with each page. Over time, this led to the 90th-percentile page load time ballooning to about 10 seconds! Midway through the year, reducing that load time by half became one of the company’s three top priorities. I was on a small team of engineers tasked with making it happen.

As we investigated where most of the JavaScript was coming from, we noticed the majority of it was performing simple tasks. These tasks involved either fetching additional data or markup from the server, or submitting a form and then receiving more markup to update the page. With limited time, we decided to build a small solution to abstract those patterns and reduce the amount of code needed on the page.

Tom Occhino and I built the first version of Primer and converted a few use cases ourselves to ensure it worked well. Once we were confident, we brought more engineers into the effort to scale it across the codebase.

Q: Primer & React were both created at Facebook. Was there any internal competition or discussion between the teams? What did that look like?

The two projects came from different eras, needs, and parts of the codebase. As far as I know, there was never any competition between them.

Primer worked well for the type of website we were building in 2010. A key part of its success was understanding that it wasn’t meant to handle every use case. It was an 80/20 solution, and we didn’t use it for particularly complex interactions (like the interface for creating a new post).

React emerged from a completely different challenge: the ads tools. Managing, composing, and tracking hundreds of ads required a highly involved, complex interface. I’m not sure if they ever attempted to use Primer for it, but it would have been a miserable experience. We didn’t have the terminology at the time, but this was a classic example of a single-page application needing purpose-built tools. The users of that site also had a very different profile from someone browsing their home feed or clicking through photos.

Q: Why do you think Primer ultimately failed at Facebook?

I don’t think there’s any single technical solution that has spanned 15 years in Facebook’s platform. The site’s needs evolve, technology changes, and the internet’s landscape shifts over time. Primer served the site well for its time and constraints, but eventually, the product demanded richer interactivity, which wasn’t what Primer was designed for.

Other tradeoffs also come into play: developer ease/speed, security, scalability. These priorities and tradeoffs change over time, especially as a company grows 10x in size.

More broadly, these things tend to work in cycles in the industry. Streamlined, fast solutions give way to richer, heavier tools, which eventually cycle back to streamlined and fast. I wouldn’t be surprised if something like Primer made a comeback at some point.

Q: How much “theory” was there to Primer? Did you think much about hypermedia, REST, etc., when you were building it?

Not much. Honestly, I was young and didn’t know a ton about the internet’s history or past research. I was drawn to the simplicity of the web’s underlying building blocks and thought it was fun to use those tools as they were designed. But, as always, the web is a layer cake of hacks and bandaids, so you have to be flexible.

Q: What were the most important technical lessons you took away from Primer?

Honestly, the biggest lessons were about people. Building a system like Primer is one thing, but for it to succeed, you have to train hundreds of engineers to use it. You have to teach them to think differently about building things, ask questions at the right time, and avoid going too far in the wrong direction. At the end of the day, even if the system is perfect, if engineers hate using it, it won’t succeed.
</Release>

<Release date="January 27, 2025" published="2025-01-27T00:00:00.000Z" url="https://htmx.org/essays/interviews/mike-amundsen/">
## An interview with Mike Amundsen, Author of 'RESTful Web APIs'

Mike Amundsen is a computer programmer, author and speaker, and is one of the world leading experts on REST &
hypermedia. He has been writing about REST and Hypermedia since 2008 and has published two books on the ideas:

- [RESTful Web APIs](http://restfulwebapis.com/)

- [Building Hypermedia APIs with HTML and Node](http://www.dpbolvw.net/click-7269430-11260198?sid=HP&url=http%3A%2F%2Fshop.oreilly.com%2Fproduct%2F0636920020530.do%3Fcmp%3Daf-prog-book-product_cj_9781449306571_%25zp&cjsku=0636920020530)

Mike agreed to do an interview with me on his view of the history of hypermedia and where things are today.

**Q**: The “standard” history of hypermedia is Vannevar Bush’s “As We May Think”, followed by Nelson introducing
the term “hypermedia” in 1963, Englebart’s “Mother of all Demos” in 1968 and then Berners-Lee creating The Web in 1990.
Are there any other important points you see along the way?

I think starting the history of what I call the “modern web” with Bush makes a lot of sense. Primarily because you can
directly link Bush to Engelbart to Nelson to Berners-Lee to Fielding. That’s more than half a century of scholarship,
design, and implementation that we can study, learn from, and expand upon.

At the same time, I think there is an unsung hero in the hypermedia story; one stretches back to the early 20th century.
I am referring to the Belgian author and entrepreneur [Paul Otlet](https://en.wikipedia.org/wiki/Paul_Otlet). Otlet had
a vision of [a multimedia information system](https://monoskop.org/Mundaneum_symposium) he named the “World Wide
Network”. He saw how we could combine text, audio, and video into a mix of live and on-demand replay of content from
around the world. He even envisioned a kind of multimedia workstation that supported searching, storing, and playing
content in what was the earliest instance I can find of an understanding of what we call “streaming services” today.

To back all this up, he
created [a community of researchers](https://daily.jstor.org/internet-before-internet-paul-otlet/) that would read
monographs, articles, and books then summarize them to fit on a page or less. He then designed an identification
system – much like our URI/URN/URLs today and created a massive card catalog system to enable searching and collating
the results into a package that could be shared – even by postal service – with recipients. He created web search by
mail in the 1920s!

This was a man well ahead of his time that I’d like to see talked about more in hypermedia and information system
circles.

**Question**: Why do you think that The Web won over other hypermedia systems (such as Xanadu)?

The short reason is, I think, that Xanadu was a much more detailed and specific way of thinking about linking documents,
documenting provenance, and compensating authors. That’s a grand vision that was difficult to implement back in the 60s
and 70s when Nelson was sharing his ideas.

There are, of course, lots of other factors. Berners-Lee’s vision was much smaller (he was trying to make it easy for
CERN staff to share contact information!). Berners-Lee was, I think, much more pragmatic about the implementation
details. He himself said he used existing tech (DNS, packet networking, etc.) to implement his ideas. That meant he
attracted interest from lots of different communities (telephone, information systems, computing, networking, etc.).

I would also say here that I wish [Wendy Hall](https://en.wikipedia.org/wiki/Wendy_Hall)
’s [Microcosm](https://www.sciencefriday.com/articles/the-woman-who-linked-the-web-in-a-microcosm/) had gotten more
traction than it did. Hall and her colleagues built an incredibly rich hypermedia system in the 90s and released it
before Berners-Lee’s version of “the Web” was available. And Hall’s Microcosm held more closely to the way Bush,
Englebart, and Nelson thought hypermedia systems would be implemented – primarily by storing the hyperlinks in a
separate “anchor document” instead of in the source document itself.

**Question**: What do you think of my essay “How did REST come to mean the opposite of REST”? Are there any points you
disagree with in it?

I read that piece back in 2022 when you released it and enjoyed it. While I have nothing to quibble with, really, there
are a few observations I can share.

I think I see most hypermedia developers/researchers go through a kind of cycle where you get exposed to “common” REST,
then later learn of “Fielding’s REST” and then go back to the “common REST” world with your gained knowledge and try to
get others on board; usually with only a small bit of success.

I know you like memes so, I’ll add mine here. This journey away from home, into expanded knowledge and the return to the
mundane life you once led is – to me – just another example of Campbell’s Hero’s Journey. I feel this so strongly
that I created [my own Hero’s Journey presentation](http://amundsen.com/talks/2015-05-barcelona/index.html) to deliver
at API conferences over the years.

On a more direct note. I think many readers of Fielding’s Dissertation (for those who actually read it) miss some key
points. Fielding’s paper is about designing network architecture, not about REST. REST is offered as a real-world
example but it is just that; an example of his approach to information network design. There have been other designs
from the same school (UC Irvine) including Justin Erenkrantz’s Computational
REST ([CREST](https://www.erenkrantz.com/CREST/)) and Rohit Kare’s Asynchronous REST (A-REST). These were efforts that
got the message of Fielding: “Let’s design networked software systems!”

But that is much more abstract work that most real-world developers need to deal with. They have to get code out the
door and up and running quickly and consistently. Fielding’s work, he admitted, was on
the “[scale of decades](https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven#comment-724)” – a scale
most developers are not paid to consider.

In the long run, I think it amazing that a PhD dissertation from almost a quarter-century ago has had such a strong
influence on day-to-day developers. That’s pretty rare.

**Question**: Hyperview, the mobile hypermedia that Adam Stepinski created, was very explicitly based on your books.
Have you looked at his system?

I have looked over [Hyperview](https://hyperview.org/) and like what I see. I must admit, however, that I don’t write
mobile code anymore so I’ve not actually written any hyperview code myself. But I like it.

I talked to Adam in 2022 about Hyperview in general and was impressed with his thoughts. I’d like to see more people
talking about and using the Hyperview approach.

Something I am pretty sure I mentioned to Adam at the time is that Hyperview reminds me of Wireless Markup
Language ([WML](https://en.wikipedia.org/wiki/Wireless_Markup_Language)). This was another XML-based document model
aimed at rendering early web content on feature phones (before smartphone technology). Another XML-based hypermedia
domain-specific document format is [VoiceXML](https://en.wikipedia.org/wiki/VoiceXML). I still think there are great
applications of hypermedia-based domain-specific markup languages (DSML) and would like to see more of them in use.

**Question**: It’s perhaps wishful thinking, but I feel there is a resurgence in interest in the ideas of hypermedia and
REST (real REST.)  Are you seeing this as well? Do you have a sense if businesses are starting to recognize the
strengths of this approach?

I, myself, think there is a growth in hypermedia-inspired designs and implementations and I’m glad to see it. I think
much of the work of APIs in general has been leading the market to start thinking about how to lower the barrier of
entry for using and interoperating with remote, independent services. And the hypermedia control paradigm (the one you
and your colleagues talk about in your
paper “[Hypermedia Controls: Feral to Formal](https://dl.acm.org/doi/fullHtml/10.1145/3648188.3675127)”) offers an
excellent way to do that.

I think the biggest hurdle for using more hypermedia in business is
was [laid out pretty conclusively](https://www.crummy.com/writing/speaking/2015-RESTFest/)
by [Leonard Richardson](https://www.crummy.com/self/) several years ago. He helped build a
powerful [hypermedia-based book-sharing server and client](https://opds.io/) system to support public libraries around
the world. He noted that, in the library domain, each site is not a competitor but a partner. That means libraries are
encouraged to make it easier to loan out books and interoperate with other libraries.

Most businesses operate on the opposite model. They typically succeed by creating barriers of entry and by hoarding
assets, not sharing them. Hypermedia makes it easier to share and interact without the need of central control or other
types of “gatekeeping.”

Having said that, I think a ripe territory for increased use of hypermedia to lower the bar and increase interaction is
at the enterprise level in large organizations. Most big companies spend huge amounts of money building and rebuilding
interfaces in order to improve their internal information system. I can’t help but think designing and implementing
hypermedia-driven solutions would yield long-term savings, and near-term sustainable interoperability.

**Question**: Are there any concepts in hypermedia that you think we are sleeping on? Or, maybe said another way, some
older ideas that are worth looking at again?

Well, as I just mentioned, I think hypermedia has a big role to play in the field of interoperability. And I think the
API-era has, in some ways, distracted us from the power of hypermedia controls as a design element for
service-to-service interactions.

While I think Nelson, Berners-Lee and others have done a great job of laying out the possibilities for human-to-machine
interaction, I think we’ve lost sight of the possibilities hypermedia gives us for machine-to-machine interactions. I am
surprised we don’t have more hypermedia-driven workflow systems available today.

And I think the rise in popularity of LLM-driven automation is another great opportunity to create hypermedia-based,
composable services that can be “orchestrated” on the fly. I am worried that we’ll get too tied up in trying to make
generative AI systems look and act like human users and miss the chance to design hypermedia workflow designed
specifically to take advantage of the strengths of statistical language models.

I’ve seen some interesting things in this area including [Zdenek Nemec](https://www.linkedin.com/in/zdne/)’s
[Superface](https://superface.ai/) project which has been working on this hypermedia-driven workflow for several
years.

I just think there are lots of opportunities to apply what we’ve learned from the last 100 years (when you include
Otlet) of hypermedia thinking. And I’m looking forward to seeing what comes next.
</Release>

<Release date="January 27, 2025" published="2025-01-27T00:00:00.000Z" url="https://htmx.org/essays/interviews/chris-wanstrath/">
## An interview with Chris Wanstrath aka @defunkt, Creator of pjax

I’m very excited to be able to interview @defunkt, the author of [pjax](https://github.com/defunkt/jquery-pjax), an
early hypermedia-oriented javascript library that served as an inspiration for intercooler.js, which later became
htmx. He’s done a few other things too, like co-founding GitHub, but in this interview I want to focus on pjax, how it
came to be, what influenced it and what it in turn influenced.

Thank you for agreeing to an interview @defunkt!

Q: To begin with, why don’t you give the readers a bit of your background both professionally & technically:

I think I can sum up most of my technical background in two quick anecdotes:

- 

For “show and tell” in 6th grade, I brought in a printout of a web page I had made - including its source code. I
like to imagine that everyone was impressed.

- 

Right after 7th grade, a bunch of rowdy high schoolers took me down to the local university during a Linux
installfest and put Red Hat on my family’s old PC. That became my main computer for all of high school.

So pretty much from the start I was a web-slinging, UNIX-loving hippie.

In terms of coding, I started on QBasic using the IBM PC running OS/2 in my grandparents’ basement. Then I got deep into
MUDs (and MUSHes and MUXes and MOOs…) which were written in C and usually included their own custom scripting
language. Writing C was “hardcoding”, writing scripts was “softcoding”. I had no idea what I was doing in C, but I
really liked the softcoding aspect.

The same rowdy high schoolers who introduced me to Linux gave me the O’Reilly camel book and told me to learn Perl. I
did not enjoy it. But they also showed me php3, and suddenly it all came together: HTML combined with MUD-like
softcoding. I was hooked.

I tried other things like ASP 3.0 and Visual Basic, but ultimately PHP was my jam for all of high school. I loved making
dynamic webpages, and I loved Linux servers. My friends and I had a comedy website in high school that shall remain
nameless, and I wrote the whole mysql/php backend myself before blogging software was popular. It was so much fun.

My first year of college I switched to Gentoo and became fascinated with their package manager, which was written in
Python. You could write real Linux tools with it, which was amazing, but at the time the web story felt weak.

I bought the huge Python O’Reilly book and was making my way through it when, randomly, I discovered Ruby on Rails. It
hit me like a bolt of lightning and suddenly my PHP and Python days were over.

At the same time, Web 2.0 had just been coined and JavaScript was, like, “Hey, everyone. I’ve been here all along.” So
as I was learning Rails, I was also learning JavaScript. Rails had helpers to abstract the JS away, but I actually
really liked the language (mostly) and wanted to learn it without relying on a framework or library.

The combination of administering my own Linux servers, writing backend code in Rails, and writing frontend code in
JavaScript made me fall deeper in love with the web as a platform and exposed me to concepts like REST and HATEOAS.
Which, as someone who had been writing HTML for over a decade, felt natural and made sense.

GitHub launched in 2008 powered by, surprise, Gentoo, Rails, and JavaScript. But due to GitHub’s position as not just a
Rails community, but a collection of programming communities, I quickly evolved into a massive polyglot.

I went back and learned Python, competing in a few programming competitions like Django Dash and attending (and
speaking) at different PyCons. I learned Objective-C and made Mac (and later iPhone) apps. I learned Scheme and Lisp,
eventually switching to Emacs from Vim and writing tons of Emacs Lisp. I went back and learned what all the sigils mean
in Perl. Then Lua, Java, C++, C, even C# - I wanted to try everything.

And I’m still that way today. I’ve written projects in Go, Rust, Haskell, OCaml, F#, all sorts of Lisps (Chicken Scheme,
Clojure, Racket, Gambit), and more. I’ve written a dozen programming languages, including a few that can actually do
something. Right now I’m learning Zig.

But I always go back to the web. It’s why I created the Atom text editor using web technologies, it’s why Electron
exists, and it’s why I just cofounded the Ladybird Browser Initiative with Andreas Kling to develop the independent,
open source Ladybird web browser.

Q: Can you give me the history of how pjax came to be?

It all starts with XMLHttpRequest, of course. Ajax. When I was growing up, walking to school both ways uphill in the
snow, the web was simple: you clicked on a link and a new web page loaded. Nothing fancy. It was a thing of beauty, and
it was good.

Then folks started building email clients and all sorts of application-like programs in HTML using `` and
friends. It was not very beautiful, and not very good, but there was something there.

Luckily, in the mid-2000s, Gmail and Ajax changed things. Hotmail had been around for a while, but Gmail was fast. By
updating content without a full page load using XMLHttpRequest, you could make a webpage that felt like a desktop
application without resorting to frames or other chicanery. And while other sites had used Ajax before Gmail, Gmail
became so popular that it really put this technique on the map.

Soon Ajax, along with the ability to add rounded corners to web pages, ushered in the era known as Web 2.0. By 2010,
more and more web developers were pushing more and more of their code into JavaScript and loading dynamic content with
Ajax. There was just one problem: in the original, good model of the web, each page had a unique URL that you could use
to load its content in any context. This is one of the innovations of the web. When using Ajax, however, the URL doesn’t
change. And even worse, it can’t be changed - not the part that gets read by the server, anyway. The web was broken.

As is tradition, developers created hacks to work around this limitation. The era of the #! began, pioneered by
Ajax-heavy sites like Facebook and Twitter. Instead of http://twitter.com/htmx_org, you’d
see http://twitter.com/#!/htmx_org in your browser’s URL bar when visiting someone’s profile. The # was traditionally
used for anchor tags, to link to a sub-section within a full web page, and could be modified by JavaScript. These
ancient web 2.0 developers took advantage of #’s malleability and started using it to represent permanent content that
could be updated inline, much like a real URL. The only problem was that your server code never saw the # part of a URL
when serving a request, so now you needed to start changing your backend architecture to make everything work.

Oh, and it was all very buggy. That was a problem too.

As an HTTP purist, I detested the #!. But I didn’t have a better way.

Time passed and lo, a solution appeared. One magical day, the #!s quietly disappeared from Facebook, replaced by good
old fashioned URLs. Had they abandoned Web 2.0? No… they had found a better way.

The `history.pushState()` function, along with its sibling `history.replaceState()`, had been recently added to all
major web browsers. Facebook quickly took advantage of this new API to update the full URL in your browser whenever
changing content via Ajax, returning the web to its previous glory.

And so there it was: the Missing Link.

We had our solution, but now a new problem: GitHub was not an SPA, and I didn’t want it to be one. By 2011 I had been
writing JavaScript for six years - more than enough time to know that too much JS is a terrible thing. The original
GitHub Issue Tracker was a Gmail-style web application built entirely in JS, circa 2009. It was an awful experience for
me, GitHub developers, and, ultimately, our users.

That said, I still believed Ajax could dramatically speed up a web page’s user interface and improve the overall
experience. I just didn’t want to do it by writing lots of, or any, JavaScript. I liked the simple request/response
paradigm that the web was built on.

Thus, Pjax was born. It sped up GitHub’s UI by loading new pages via Ajax instead of full page loads, correctly updating
URLs while not requiring any JS beyond the Pjax library itself. Our developers could just tag a link with `[data-pjax]`
and our backend application would then automatically render a page’s content without any layout, quickly getting you
just the data you need without asking the browser to reload any JS or CSS or HTML that didn’t need to change. It also (
mostly) worked with the back button, just like regular web pages, and it had a JS API if you did need to dip into the
dark side and write something custom.

The first commit to Pjax was Feb 26, 2011 and it was released publicly in late March 2011, after we had been using it to
power GitHub.com for some time.

Q: I recall it being a big deal in the rails community. Did the advent of turbolinks hurt adoption there?

My goal wasn’t really adoption of the library. If it was, I probably would have put in the work to decouple it from
jQuery. At the time, I was deep in building GitHub and wasn’t the best steward of my many existing open source projects.

What I wanted instead was adoption of the idea - I wanted people to know about `pushState()`, and I wanted people to
know there were ways to build websites other than just doing everything by hand in JavaScript. Rendering pages in whole
or in part on the server was still viable, and could be sped up using modern techniques.

Turbolinks being created and integrated into Rails was amazing to see, and not entirely unsurprising. I was a huge fan
of Sam Stephenson’s work even pre-GitHub, and we had very similiar ideas about HTTP and the web. Part of my thinking was
influenced by him and the Rails community, and part of what drew me to the Rails community was the shared ideas around
what’s great about the web.

Besides being coupled to jQuery, pjax’s approach was quite limited. It was a simple library. I knew that other people
could take it further, and I’m glad they did.

Q: How much “theory” was there to pjax? Did you think much about hypermedia, REST, etc. when you were building it? (
I backed into the theory after I had built intercooler, curious how it went for you!)

Not much. It started by appending `?pjax=1` to every request, but before release we switched it to send an `X-PJAX`
header instead. Very fancy.

Early GitHub developer Rick Olson (@technoweenie), also from the Rails community, was the person who introduced me to
HATEOAS and drove that philosophy in GitHub’s API. So anything good about Pjax came from him and Josh Peek, another
early Rails-er.

My focus was mostly on the user experience, the developer experience, and trying to stick to what made the web great.

- First commit: https://github.com/defunkt/jquery-pjax/commit/3efcc3c

- X-PJAX: https://github.com/defunkt/jquery-pjax/commit/4367ec9
</Release>

<Release date="January 10, 2025" published="2025-01-10T00:00:00.000Z" url="https://htmx.org/essays/a-real-world-wasm-to-htmx-port/">
## A Real World wasm to htmx Port

img, video {
  max-width: 100%;
  margin: 10px;
}

When I was in college, I wrote some customer service software that tied together some custom AI models I trained, the OpenAI API, a database, and some social media APIs to make the first version of [Sidekick](https://sidekickai.co).

## Led astray

Over the next couple years I worked on adding more features and growing the user base. As a solo founder, I should have been focused on sales, marketing, and market discovery. Instead, as an engineer, I wanted to hand-craft the perfect web stack. I was firmly of the belief that the network gap between the frontend and the backend could be abstracted away, and I could make writing web apps as simple as writing native apps. Did this have anything to do with my business, product, or customers? Absolutely not, but as many technical founders do, I believed if I perfected the tech, the customers would materialize.

My design decisions were naive, but also reminiscent to what’s seen in industry today: I wanted the backend and frontend to share a language (Rust), I wanted compile-time checks across the network boundary, I wanted to write frontend code like it was an app (reactive), and I wanted nearly instant reload times. What I got out of it was a buggy mess.

I had invented a system where simple rust functions can be tagged with a macro to generate a backend route and a frontend request function, so you can call the function like it was a standard function, and it would run on the backend. A true poor-mans GraphQL. My desire to write Rust on the frontend required I compile a WASM bundle. My desire for instant load times required isomorphic SSR. All of this complexity, for what was essentially a simple CRUD site.

## A better way

At this point Sidekick has grown and it now has a codebase which is responsible for not-insignificant volumes of traffic each day. There was this point where I looked into HTMX, multi-page websites, and HATEOAS, and realized the Sidekick codebase, which had grown into ~36k lines spread over 8 different crates, could be folded into a single crate, a single binary that ran the backend, which generated the frontend on demand through templating, and that HTMX could suffice for all the interactivity we required.

Large refactors typically have a bad track record so we wrote a quick and dirty simplified version of part of the site to convince ourselves it could work. After sufficient convincing, we undertook a full rewrite. All said and done, the rewrite took approximately 3 weeks of intense work. The results were dramatic:

- **36k LOC -> 8k LOC**

- **8 crates -> 1 crate**

- **~5 bug reports / week -> ~1 bug report / week**

- **More full nights of sleep**

![sidekick_port_loc.jpg](/img/sidekick_port_loc.jpg)

The rewrite went far better than I could have imagined. It definitely won’t be representative of every experience, our app was definitely uniquely suited to HTMX. Axum and some custom middleware also went a long way for sharing common infrastructure across the site. Though we don’t have proper metrics, we’ve anecdotally noticed significantly improved load times.

## Reflection

I’ll finish by touching on the biggest benefit in my eyes: it’s tremendously easier to add new features as our customers request them. A feature that would have taken 2 weeks to fully implement, test and ship, now takes a day or two. As a small startup with a large number of customer demands, this is table stakes.

Sidekick hasn’t raised VC funding so I can’t afford to hire lots of devs. With HTMX we don’t need to.
</Release>

<Release date="January 1, 2025" published="2025-01-01T00:00:00.000Z" url="https://htmx.org/essays/future/">
## The future of htmx

## In The Beginning…

htmx began life as [intercooler.js](https://intercoolerjs.org), a library built around jQuery that added behavior based
on HTML attributes.

For developers who are not familiar with it, [jQuery](https://jquery.com/) is a venerable JavaScript
library that made writing cross-platform JavaScript a lot easier during a time when browser implementations were very
inconsistent, and JavaScript didn’t have many of the convenient APIs and features that it does now.

Today many web developers consider jQuery to be “legacy software.” With all due respect to this perspective, jQuery is
currently used on [75% of all public websites](https://w3techs.com/technologies/overview/javascript_library), a number that dwarfs all other JavaScript tools.

Why has jQuery remained so ubiquitous?

Here are three technical reasons we believe contribute to its ongoing success:

- It is very easy to add to a project (just a single, dependency-free link)

- It has maintained a very consistent API, remaining largely backwards compatible over its life (intercooler.js works
with jQuery v1, v2 and v3)

- As a library, you can use as much or as little of it as you like: it stays out of the way otherwise and doesn’t
dictate the structure of your application

## htmx is the New jQuery

Now, that’s a ridiculous (and arrogant) statement to make, of course, but it is an *ideal* that we on the htmx team are
striving for.

In particular, we want to emulate these technical characteristics of jQuery that make it such a low-cost, high-value
addition to the toolkits of web developers. Alex has
discussed [“Building The 100 Year Web Service”](https://www.youtube.com/watch?v=lASLZ9TgXyc) and we want htmx to be a
useful tool for exactly that use case.

Websites that are built with jQuery stay online for a very long time, and websites built with htmx should be capable of
the same (or better).

Going forward, htmx will be developed with its *existing* users in mind.

If you are an existing user of htmx—or are thinking about becoming one—here’s what that means.

### Stability as a Feature

We are going to work to ensure that htmx is extremely stable in both API & implementation. This means accepting and
documenting the [quirks](https://htmx.org/quirks/) of the current implementation.

Someone upgrading htmx (even from 1.x to 2.x) should expect things to continue working as before.

Where appropriate, we may add better configuration options, but we won’t change defaults.

### No New Features as a Feature

We are going to be increasingly inclined to not accept new proposed features in the library core.

People shouldn’t feel pressure to upgrade htmx over time unless there are specific bugs that they want fixed, and they
should feel comfortable that the htmx that they write in 2025 will look very similar to htmx they write in 2035 and
beyond.

We will consider new core features when new browser features become available, for example we
are [already using](https://htmx.org/examples/move-before/) the experimental `moveBefore()` API on supported browsers.

However, we expect most new functionality to be explored and delivered via the
htmx [extensions API](https://htmx.org/extensions/), and will work to make the extensions API more capable where
appropriate.

### Quarterly Releases

Our release schedule is going to be roughly quarterly going forward.

There will be no death march upgrades associated with htmx, and there is no reason to monitor htmx releases for major
functionality changes, just like with jQuery. If htmx 1.x is working fine for you, there is no reason to feel like you
need to move to 2.x.

## Promoting Hypermedia

htmx does not aim to be a total solution for building web applications and services:
it [generalizes hypermedia controls](https://dl.acm.org/doi/pdf/10.1145/3648188.3675127), and that’s roughly about it.

This means that a very important way to improve htmx — and one with lots of work remaining — is by helping improve the tools
and techniques that people use *in conjunction* with htmx.

Doing so makes htmx dramatically more useful *without* any changes to htmx itself.

### Supporting Supplemental Tools

While htmx gives you a few new tools in your HTML, it has no opinions about other important aspects of building your
websites. A flagship feature of htmx is that it does not dictate what backend or database you use.

htmx is [compatible with lots of backends](https://htmx.org/essays/hypermedia-on-whatever-youd-like/), and we want to
help make hypermedia-driven development work better for all of them.

One part of the hypermedia ecosystem that htmx has already helped improve is template engines. When
we [first wrote](https://htmx.org/essays/template-fragments/) about how “template fragments” make defining partial page
replacements much simpler, they were a relatively rare feature in template engines.

Not only are fragments much more common now, that essay
is [frequently](https://github.com/mitsuhiko/minijinja/issues/260) [cited](https://github.com/sponsfreixes/jinja2-fragments)
as an inspiration for building the feature.

There are many other ways that the experience of writing hypermedia-based applications can be improved, and we will
remain dedicated to identifying and promoting those efforts.

### Writing, Research, and Standardization

Although htmx will not be changing dramatically going forward, we will continue energetically evangelizing the ideas of
hypermedia.

In particular, we are trying to push [the ideas](https://dl.acm.org/doi/pdf/10.1145/3648188.3675127) of htmx into the
HTML standard itself, via the [Triptych project](https://alexanderpetros.com/triptych/). In an ideal world, htmx
functionality disappears into the web platform itself.

htmx code written *today* will continue working forever, of course, but in the very long run perhaps there will be no
need to include the library to achieve [similar UI patterns](https://htmx.org/examples) via hypermedia.

## Intercooler Was Right

At the [end of the intercooler docs](https://intercoolerjs.org/docs#philosophy), we said this:

Many javascript projects are updated at a dizzying pace. Intercooler is not.

This is not because it is dead, but rather because it is (mostly) right: the basic idea is right, and the implementation
at least right enough.

This means there will not be constant activity and churn on the project, but rather
a [stewardship](https://en.wikipedia.org/wiki/Stewardship_(theology)) relationship: the main goal now is to not screw
it up. The documentation will be improved, tests will be added, small new declarative features will be added around the
edges, but there will be no massive rewrite or constant updating. This is in contrast with the software industry in
general and the front end world in particular, which has comical levels of churn.

Intercooler is a sturdy, reliable tool for web development.

Leaving aside [the snark at the end of the third paragraph](https://www.youtube.com/watch?v=zGyAWH5btwY), this thinking
is very much applicable to htmx. In fact, perhaps even more so since htmx is a standalone piece of software, benefiting
from the experiences (and mistakes) of intercooler.js.

We hope to see htmx, in its own small way, join the likes of giants like jQuery as a sturdy and reliable tool for
building your 100 year web services.
</Release>

<Release date="December 23, 2024" published="2024-12-23T00:00:00.000Z" url="https://htmx.org/quirks/">
## htmx quirks

This is a “quirks” page, based on [SQLite’s “Quirks, Caveats, and Gotchas In SQLite” page](https://www.sqlite.org/quirks.html).

## Attribute Inheritance

Many attributes in htmx are [inherited](https://htmx.org/docs/#inheritance): child elements can receive behavior from attributes located
on parent elements.

As an example, here are two htmx-powered buttons that inherit their [target](https://htmx.org/attributes/hx-target/) from a parent
div:

```
div hx-target="#output">
    button hx-post="/items/100/like">Likebutton>
    button hx-delete="/items/100">Deletebutton>
div>
output id="output">output>

```

This helps avoid repeating attributes, thus keeping code [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).

On the other hand, as the attributes get further away elements, you lose [Locality of Behavior](https://htmx.org/essays/locality-of-behaviour/)
and it becomes more difficult to understand what an element is doing.

It is also possible to inadvertently change the behavior of elements by adding attributes to parents.

Some people prefer to disable inheritance in htmx entirely, using the `htmx.config.disableInheritance`
[configuration variable](https://htmx.org/docs/#config).

Here is a `meta` tag configuration that does so:

```
  meta name="htmx-config" content='{"disableInheritance":true}'>

```

## The Default Swap Strategy is `innerHTML`

The [`hx-swap`](https://htmx.org/attributes/hx-swap/) attribute allows you to control how a swap is performed.  The default strategy is
`innerHTML`, that is, to place the response HTML content within the target element.

Many people prefer to use the `outerHTML` strategy as the default instead.

You can change this behavior using the `htmx.config.defaultSwapStyle`
[configuration variable](https://htmx.org/docs/#config).

Here is a `meta` tag configuration that does so:

```
  meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>

```

## Targeting the `body` Always Performs an innerHTML Swap

For historical reasons, if you target the `body` element, htmx will
[always perform an `innerHTML` swap](https://github.com/bigskysoftware/htmx/blob/fb78106dc6ef20d3dfa7e54aca20408c4e4336fc/src/htmx.js#L1696).

This means you cannot change attributes on the `body` tag via an htmx request.

## By Default `4xx` & `5xx` Responses Do Not Swap

htmx has never swapped “error” status response codes (`400`s & `500`s) by default.

This behavior annoys some people, and some server frameworks, in particular, will return a `422 - Unprocessable Entity`
response code to indicate that a form was not filled out properly.

This can be very confusing when it is first encountered.

You can configure the response behavior of htmx via the [`htmx:beforeSwap`](https://htmx.org/docs/#modifying_swapping_behavior_with_events)
event or [via the `htmx.config.responseHandling` config array](https://htmx.org/docs/#response-handling).

Here is the default configuration:

```
{
  "responseHandling": [
    {"code":"204", "swap": false},
    {"code":"[23]..", "swap": true},
    {"code":"[45]..", "swap": false, "error":true},
    {"code":"...", "swap": false}]
}

```

Note that `204  No Content` also is not swapped.

If you want to swap everything regardless of response code, you can use this configuration:

```
{
  "responseHandling": [
    {"code":"...", "swap": true}]
}

```

If you want to specifically allow `422` responses to swap, you can use this configuration:

```
{
  "responseHandling": [
    {"code":"422", "swap": true},
    {"code":"204", "swap": false},
    {"code":"[23]..", "swap": true},
    {"code":"[45]..", "swap": false, "error":true},
    {"code":"...", "swap": false}]
}

```

Here is a meta tag allowing all responses to swap:

```
  meta name="htmx-config" content='{"responseHandling": [{"code":"...", "swap": true}]}'>

```

## `GET` Requests on Non-Form Elements Do Not Include Form Values by Default

If a non-form element makes a non-`GET` request (e.g. a `PUT` request) via htmx, the values of the enclosing form
of that element (if any) [will be included in the request](https://htmx.org/docs/#parameters).

However, if the element issues a `GET`, the values of an enclosing form will
[not be included.](https://github.com/bigskysoftware/htmx/blob/fb78106dc6ef20d3dfa7e54aca20408c4e4336fc/src/htmx.js#L3525)

If you wish to include the values of the enclosing form when issuing an `GET` you can use the
[`hx-include`](https://htmx.org/attributes/hx-include/) attribute like so:

```
button hx-get="/search"
        hx-include="closest form">
  Search
button>

```

## History Can Be Tricky

htmx provides support for interacting with the browser’s [history](https://htmx.org/docs/#history).  This can be very powerful, but it
can also be tricky, particularly if you are using 3rd party JavaScript libraries that modify the DOM.

There can also be [security concerns](https://htmx.org/docs/#hx-history) when using htmx’s history support.

Most of these issues can be solved by disabling any local history cache and simply issuing a server request when a
user navigates backwards in history, with the tradeoff that history navigation will be slower.

Here is a meta tag that disables history caching:

```
  meta name="htmx-config" content='{"historyCacheSize": 0}'>

```

## Some People Don’t Like `hx-boost`

[`hx-boost`](https://htmx.org/attributes/hx-boost/) is an odd feature compared with most other aspects of htmx: it “magically” turns
all anchor tags and forms into AJAX requests.

This can speed the feel of these interactions up, and also allows the forms and anchors to continue working when
[JavaScript is disabled](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement), however it comes
with some tradeoffs:

- The history issues mentioned above can show up

- Only the body of the web page will be updated, so any styles and scripts in the new page `head` tag will be discarded

- The global javascript scope is not refreshed, so it is possible to have strange interactions between pages.  For example
a global `let` may start failing because a symbol is already defined.

Some members on the core htmx team feel that, due to these issues, as well as the fact that browsers have improved
quite a bit in page navigation, it is best to avoid `hx-boost` and
[just use unboosted links and forms](https://unplannedobsolescence.com/blog/less-htmx-is-more/).

There is no doubt that `hx-boost` is an odd-man out when compared to other htmx attributes and suffers from the dictum
that “If something magically works, then it can also magically break.”

Despite this fact, I (Carson) still feel it is useful in many situations, and it is used on the [https://htmx.org](https://htmx.org)
website.

## Loading htmx asynchronously is unreliable

htmx is designed to be loaded with a standard, blocking `` tag, not one that is a [module](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#module) or [deferred](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#defer).
Although we make a [best-effort attempt](https://github.com/bigskysoftware/htmx/blob/7ae66f9b33a5d39ad4084b0697ea34a6bf559cda/src/htmx.js#L5039-L5058) to initialize htmx regardless of when in the document lifecycle the script is loaded, there are some use-cases that slip through the cracks, typically ones that involve bundling or AJAX insertion of htmx itself.

Our [past attempts](https://github.com/bigskysoftware/htmx/pull/3365#issuecomment-3065080028) to close this gap have all lead to unacceptable regressions.
Therefore, although htmx can be loaded asynchronously, do so at your own risk.

Keep in mind, also, that if your DOM content loads before htmx does, all the htmx-provided functionality will be nonfunctional until htmx loads.
[Prefetching](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/prefetch) (or even “regular” fetching) htmx before you need it is one possible way to resolve this problem.

## The JavaScript API Is Not A Focus

htmx is a hypermedia-oriented front end library.  This means that htmx enhances HTML via
[attributes](https://htmx.org/reference/#attributes) in the HTML , rather than providing an elaborate
JavaScript API.

There *is* a [JavaScript API](https://htmx.org/reference/#api), but it is not a focus of the library and, in most cases,
should not be used heavily by htmx end users.

If you find yourself using it heavily, especially the [`htmx.ajax()`](https://htmx.org/api/#ajax) method, it may be
worth asking yourself if there is a more htmx-ish approach to achieve what you are doing.
</Release>

<Release date="December 17, 2024" published="2024-12-17T00:00:00.000Z" url="https://htmx.org/essays/lore/">
## htmx lore

![I lied, I don](/img/i-lied.png)

For better or [for worse](https://x.com/IroncladDev/status/1866185587616596356), htmx has collected a lot of lore, mainly around [the twitter account](https://twitter.com/htmx_org).

Here are some explanations.

## It’s So Over/We’re So Back

A common set of phrases used by htmx enthusiasts when, for example, [@bunjavascript told me to delete my account](https://x.com/bunjavascript/status/1708557665268568412)

## htmx CEO

At one point there was a hostile takeover attempt of the htmx CEO position and, in a desperate poison pill, I declared
everyone CEO of htmx.

[Turk](https://x.com/gitpush_gitpaid) created [https://htmx.ceo](https://htmx.ceo) if you want to register as a CEO.

If someone emails hr@bigsky.software asking if you are CEO of htmx, I will tell them yes.

You can put it on your LinkedIn, because it’s true.

## Laser Eye Horse

At some point I photoshopped lasers onto a horse mask, as kind of an homage to [@horse_js](https://x.com/horse_js).

For some reason it stuck and now it’s the [official unofficial](https://swag.htmx.org/products/i-lied-sticker) mascot of htmx.

## Spieltrieb

Spieltrieb means “play instinct”, and is a big part of the [htmx vibe](https://x.com/search?q=spieltrieb%20from%3Ahtmx_org&src=typed_query).

## Pickles

At some point someone (I think [@techsavvytravvy](https://x.com/techsavvytravvy)), generated [a grug AI image](https://x.com/htmx_org/status/1708697536587047142), and there
was a pickle smiling in a really bizarre way in it.

So we started riffing on pickles and now [there’s a shirt](https://swag.htmx.org/products/htmx-pickle-shirt).

Cry more, [drizzle](https://x.com/DrizzleORM/status/1757149983713665238).

## XSS

In July 2023, when htmx first got popular, there was a
[moral panic](https://x.com/htmx_org/status/1683607693246775297) around
[cross site scripting](https://x.com/htmx_org/status/1683529221195571200).  I
[may](https://x.com/htmx_org/status/1683607217499414531) have
[overcooked](https://x.com/htmx_org/status/1683649190071791617) my
[response](https://x.com/htmx_org/status/1683612179512057856) to
[it](https://x.com/htmx_org/status/1683818711763877892).

## Shut Up Warren

[@WarrenInTheBuff](https://x.com/WarrenInTheBuff) is the king of twitter and we regularly fight with him.  This often
ends in someone saying [“shut up warren”](https://x.com/ThePrimeagen/status/1792564215749779515).

You can see the htmx website do this by going to [https://htmx.org?suw=true](https://htmx.org?suw=true)

## Microsoft Purchase Rumor

In mid-January of 2024 I got really serious with the htmx twitter account and started [quote](https://x.com/htmx_org/status/1745930477825868044)
tweeting [things](https://x.com/htmx_org/status/1745915394626351315)
[about microsoft](https://x.com/htmx_org).  People started [worrying](https://x.com/SusSoftware/status/1746206195461878113).  I announced a [license change](https://x.com/htmx_org/status/1746736273728094323)
to get people freaked out about a rug pull.

[I then changed htmx to BSD0](https://x.com/htmx_org/status/1746880860723544211)

[This is the offer](https://x.com/htmx_org/status/1746895016256328079) I got from microsoft (real).

## (same thing)

I believe that [this tweet](https://x.com/htmx_org/status/1672264927136952322) is the origin of the (same thing) meme

## Stronger Together

In December 2023, I was trying to get some indonesian twitter users to take a look at htmx, so I created a
[“Montana & Indonesia, Stronger Together!”](https://x.com/htmx_org/status/1734371865156563428) tweet w/an AI image.

This turned into a [whole series of tweets](https://x.com/search?q=%22stronger%20together%22%20from%3A%40htmx_org&src=typed_query&f=live).

## Hinges

Sometimes I am accused of being “unhinged” but, in fact, [I own many hinges](https://x.com/search?q=from%3Ahtmx_org%20hinges&src=typed_query).

## * library

People often [call htmx a framework](https://htmx.org/essays/is-htmx-another-javascript-framework/), but it’s [a library](https://x.com/htmx_org/status/1848751101035827210)

## “man”

A common [one word response](https://x.com/search?q=%22man%22%20from%3Ahtmx_org&src=typed_query&f=live) when I don’t feel like
arguing with someone.

## The Le Marquee d’

In December 2024, I [added a marquee tag](https://github.com/bigskysoftware/htmx/commit/2b88d967c19619281228d1bf5398751615bdf462) to
the htmx website and started using the honorific (sic) in my twitter title.

## htmx sucks

I wrote an essay called [htmx sucks](https://htmx.org/essays/htmx-sucks/) in which I criticize htmx (some valid, some tongue in
cheek, most both.)  I also released [a mug](https://swag.htmx.org/products/htmx-sucks-mug) that I will often link to when people are criticizing htmx.

## Jason Knight

Jason Knight [hates htmx](https://x.com/JasonKn99664124/status/1731555036864381251) and wrote a
[great post](https://archive.is/rQrl7) about it.

Please don’t harass him, [I draw energy](https://x.com/htmx_org/status/1756476449693872635) from his posts.

## Drop Downs

In July 2023, sparked by the accusation that htmx users could not create dropdowns, I did a deep-dive into web
drop down technology and [uncovered a bombshell](https://x.com/htmx_org/status/1684936514885869568)

## “htmx is a front end library of peace”

A phrase I will often [quote tweet](https://x.com/search?q=htmx%20is%20a%20front%20end%20library%20of%20peace%20from%3A%40htmx_org&src=typed_query&f=live)
violent htmx-related imagery with.

## The Process ™

[The Process™](https://x.com/htmx_org/status/1697651918858764375) is the mechanism by which people initially hostile
to htmx come to be enlightened.

## “that’s ridiculous”

In [June 2023](https://x.com/htmx_org/status/1807183339222405317), [@srasash](https://twitter.com/srasash) accused
htmx of being a government op, the first in many such increasingly ridiculous claims.  I typically quote-tweet these
claims and point out that [“that’s ridiculous”](https://x.com/search?q=%22that%27s%20ridiculous%22%20from%3A%40htmx_org&src=typed_query&f=live)

## Grug

I created [http://grugbrain.dev](http://grugbrain.dev).

## The htmx/intercooler.js feud

The htmx & [intercooler.js](https://x.com/intercoolerjs) twitter accounts often fight with one another.  Sometimes its
just me [switching back and forth](https://x.com/intercoolerjs/status/1859652045399355559), but two other people have
access to the intercooler account, so sometimes I have no idea who I am fighting with.

## If Nothing Magically Works

Nothing [magically breaks](https://x.com/htmx_org/status/1729870461864226829).

## /r/webdev

I was very unfairly given [a lifetime ban](https://x.com/htmx_org/status/1719687461385691283) from
[/r/webdev/](https://www.reddit.com/r/webdev/) for an
[obviously satirical post](https://old.reddit.com/r/webdev/comments/17i0loi/anyone_heard_of_htmx/).  Even the term “htmx” is banned (or semi-banned) on that sub, so people now use
the [htmeggs](https://swag.htmx.org/products/htmeggs-shirt) instead.

## “looking into this”

[idk](https://x.com/search?q=%22idk%22%20from%3Ahtmx_org&src=typed_query&f=live), I just think [it’s funny](https://x.com/search?q=%22looking%20into%20this%22%20from%3Ahtmx_org&src=typed_query)

## “Look at this nerd ragin’”

A common phrase used to [mock people (including ourselves)](https://x.com/search?q=%22Look%20at%20this%20nerd%20ragin%27%22%20from%3Ahtmx_org&src=typed_query) with.

## Joker/Bane/Skeletor/Thanos, etc.

htmx is a [villain](https://x.com/htmx_org/status/1651698199478796292) in the front-end world, I’m good w/that
</Release>

<Release date="December 7, 2024" published="2024-12-07T00:00:00.000Z" url="https://htmx.org/essays/prefer-if-statements/">
## Prefer If Statements To Polymorphism...

## Or, Watching Myself Lose My Mind In Real Time…

“Invert, always invert.” –Carl Jacobi, by way of Charlie Munger

- *[If Statements](https://x.com/htmx_org/status/1843804410377535533)*

prefer if statements to polymorphism

whenever you are tempted to create a class, ask yourself: “could this be an if statement instead?”

- *[The Closed/Closed Principle](https://x.com/htmx_org/status/1843805753007845474)*

In grug-oriented programming, the closed–closed principle (CCP) states “software entities (classes, modules, functions, etc.) should be closed for extension, but also closed for modification”

they should just do something useful man

- *[The Minimize Abstractions Principle](https://x.com/htmx_org/status/1843806270559793475)*

The Minimize Abstractions Principle (MAP) is a computer programming principle that states that “a module should
minimize the number of abstractions it contains, both in API and in implementation.  Stop navel gazing nerd.”

- *[The Try It Out Substitution Principle](https://x.com/htmx_org/status/1843807054970139139)*

The “Try It Out” Substitution Principle states that you should try something out and, if that doesn’t work, think about why, and substitute something else for it instead.

It is common to need to substitute multiple things to hit on the right thing eventually.

- *[The Useful Stuff Principle](https://x.com/htmx_org/status/1843807769557909528)*

The Useful Stuff Principle states that entities must depend on useful stuff, not overly abstract nonsense. It states that  a high-level module can depend on a low-level module, because that’s how software works.

- *[Dependencies](https://x.com/htmx_org/status/1843808113230860419)*

The The Existence of Dependencies Is Not An Excuse For Destroying The Codebase Principle states that the existence of dependencies is not an excuse for destroying the codebase

- *[Abstraction Budget](https://x.com/htmx_org/status/1843821830207099007)*

consider giving your developers an abstraction budget

when they exhaust that budget & ask for more, tell them they can have another abstraction when they remove an existing one

when they complain, look for classes with the term “Factory”,  “Lookup” or “Visitor” in their names

- *[Fewer Functions](https://x.com/htmx_org/status/1843822378352291914)*

if your function is only called in one place, consider inlining it & reducing the total number of method signatures in your module, to help people better understand it

studies show longer methods have fewer bugs per line of code, so favor longer methods over many smaller methods

- *[“God” Object](https://x.com/htmx_org/status/1843823231771521367)*

consider creating “God” objects that wrap a lot of functionality up in a single package

consumers of your API don’t want to learn 50 different classes to get something done, so give them a few that provide the core functionality of your module with minimal fuss

- *[Copy & Paste Driven Development](https://x.com/htmx_org/status/1843827082687852706)*

copy&paste driven development is a development methodology where, when you need to reuse some code but in a slightly different manner, you copy & paste the code & then modify it to satisfy the new requirements

this contrasts with designing an elaborate object model, for example

- *[Implementation Driven Development](https://x.com/htmx_org/status/1843828023747063866)*

implementation driven development is a development methodology where you first explore various implementations of your idea to determine the best one, then add tests for it

no test code may be written without first having some implementation code to drive that test

- *[Mixing Concerns](https://x.com/htmx_org/status/1843830823113634132)*

Mixing of Concerns is a design methodology whereby “concerns” of various kinds are mixed into a single code unit.  This improves locality in that code unit by placing all relevant logic within it.  An example of this is hypermedia, which mixes control & presentation information.

- *[Macroservices Architecture](https://x.com/htmx_org/status/1843831529300267103)*

a macroservice architecture revolves around “macroservices”: network-deployed modules of code that provide a significant amount of functionality to the overall system

by adopting a macroservice-based architecture you minimize deployment complexity & maximize resource utilization

- *[Sorry](https://x.com/htmx_org/status/1844005320223539524)*

kinda had a manic break last night my bad
</Release>

<Release date="November 24, 2024" published="2024-11-24T00:00:00.000Z" url="https://htmx.org/essays/codin-dirty/">
## Codin' Dirty

![quick-and-dirty](/img/quick-and-dirty.png)

“Writing clean code is what you must do in order to call yourself a professional. There is no reasonable excuse for
doing anything less than your best.” [Clean Code](https://www.goodreads.com/book/show/3735293-clean-code)

In this essay I want to talk about how I write code.  I am going to call my approach “codin’ dirty” because I often
go against the recommendations of [Clean Code](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882),
a popular approach to writing code.

Now, I don’t really consider my code all that dirty: it’s a little gronky in places but for the most part I’m
happy with it and find it easy enough to maintain with reasonable levels of quality.

I’m also *not* trying to convince *you* to code dirty with this essay.  Rather, I want to
show that it is possible to write reasonably successful software this way and, I hope, offer some balance around software
methodology discussions.

I’ve been programming for a while now and I have seen a bunch of different approaches to building software work.  Some
people love Object-Oriented Programming (I like it), other very smart people hate it.  Some folks love the expressiveness
of dynamic languages, other people hate it. Some people ship successfully while strictly following Test Driven Development,
others slap a few end-to-end tests on at the end of the project, and many people end up somewhere between these extremes.

I’ve seen projects using all of these different approaches ship and maintain successful software.

So, again, my goal here is not to convince you that my way of coding is the only way, but rather to show you (particularly
younger developers, who are prone to being intimidated by terms like “Clean Code”) that you can have a successful
programming career using a lot of different approaches, and that mine is one of them.

## TLDR

Three “dirty” coding practices I’m going to discuss in this essay are:

- (Some) big functions are good, actually

- Prefer integration tests to unit tests

- Keep your class/interface/concept count down

If you want to skip the rest of the essay, that’s the takeaway.

## I Like Big Functions

I think that large functions are fine. In fact, I think that *some* big functions are usually a *good* thing in a codebase.

This is in contrast with Clean Code, which says:

“The first rule of functions is that they should be small. The second rule of functions is that they should be
smaller than that.” [Clean Code](https://www.goodreads.com/book/show/3735293-clean-code)

Now, it always depends on the type of work that I’m doing, of course, but I usually tend to organize my functions into the
following:

- A few large “crux” functions, the real meat of the module.  I set no bound on the Lines of Code (LOC) of these functions,
although I start to feel a little bad when they get larger than maybe 200-300 LOC.

- A fair number of “support” functions, which tend to be in the 10-20 LOC range

- A fair number of “utility” functions, which tend to be in the 5-10 LOC range

As an example of a “crux” function, consider the [`issueAjaxRequest()`](https://github.com/bigskysoftware/htmx/blob/7fc1d61b4fdbca486263eda79c3f31feb10af783/src/htmx.js#L4057)
in [htmx](https://htmx.org).  This function is nearly 400 lines long!

Definitely not clean!

However, in this function there is a lot of context to keep around, and it lays out a series of specific steps that must
proceed in a fairly linear manner.  There isn’t any reuse to be found by splitting it up into other functions and I
think it would hurt the clarity (and also importantly for me, the debuggability) of the function if I did so.

### Important Things Should Be Big

A big reason I like big functions is that I think that in software, all other things being equal, important things should
be big, whereas unimportant things should be little.

Consider a visual representation of “Clean” code versus “Dirty” code:

![clean-v-dirty.png](/img/clean-v-dirty.png)

When you split your functions into many equally sized, small implementations you end up smearing the important parts of your
implementation around your module, even if they are expressed perfectly well in a larger function.

Everything ends up looking the same: a function signature definition, followed by an if statement or a for loop, maybe a function
call or two, and a return.

If you allow your important “crux” functions to be larger it is easier to pick them out from the sea of functions, they
are obviously important: just look at them, they are big!

There are also fewer functions in general in all categories, since much of the code has been merged into larger functions.
Fewer lines of code are dedicated to particular type signatures (which can change over time) and it easier to keep the
important and maybe even the medium-important function names and signatures in your head.  You also tend to have fewer
LOC overall when you do this.

I prefer coming into a new “dirty” code module: I will be able to understand it more quickly and will remember the
important parts more easily.

### Empirical Evidence

What about the empirical (dread word in software!) evidence for the ideal function size?

In [Chapter 7, Section 4](https://flylib.com/books/en/2.823.1.64/1/) of [Code Complete](https://en.wikipedia.org/wiki/Code_Complete),
Steve McConnell lays out some evidence for and against longer functions.  The results are mixed, but many
of the studies he cites show better errors-per-line metrics for *larger*, rather than smaller, functions.

There are [newer studies](https://arxiv.org/pdf/2205.01842#:~:text=In%20this%20paper%20we%20examine,also%20decreases%20overall%20maintenance%20efforts)
as well that argue for smaller functions ( 200LOC, and a walk around the SQLite
codebase will furnish many other examples of large functions.  SQLite is noted for being extremely high quality and
very well maintained.

Or consider the [`ChromeContentRendererClient::RenderFrameCreated()`](https://github.com/chromium/chromium/blob/6fdb8fdff0ba83db148ff2f87105bc95e5a4ceec/chrome/renderer/chrome_content_renderer_client.cc#L591)
function in the [Google Chrome](https://www.google.com/chrome/index.html) Web Browser.  Also looks to be over 200 LOC.  Again, poking around the codebase
will give you plenty of other long functions to look at.  Chrome is solving one of the hardest problems in software:
being a good general purpose hypermedia client.  And yet their code doesn’t look very “clean” to me.

Next, consider the [`kvstoreScan()`](https://github.com/redis/redis/blob/3fcddfb61f903d7112da186cba8b1c93a99dc87f/src/kvstore.c#L359)
function in [Redis](https://redis.io/).  Smaller, on the order of 40LOC, but still far larger than Clean Code would
suggest.  A quick scan through the Redis codebase will furnish many other “dirty” examples.

These are all C-based projects, so maybe the rule of small functions only applies to object-oriented languages, like
Java?

OK, take a look at the [`update()`](https://github.com/JetBrains/intellij-community/blob/8c6cc1579ac358451ba2c5b8a54853249fdc5451/java/compiler/impl/src/com/intellij/compiler/actions/CompileAction.java#L60)
function in the `CompilerAction` class of [IntelliJ](https://www.jetbrains.com/idea/), which is roughly 90LOC.  Again,
poking around their codebase will reveal many other large functions well over 50LOC.

SQLite, Chrome, Redis & IntelliJ…

These are important, complicated, successful & well maintained pieces of software, and yet
we can find large functions in all of them.

Now, I don’t want to imply that any of the engineers on these projects agree with this essay in any way, but I think
that we have some fairly good evidence that longer functions are OK in software projects.  It seems safe to say that
breaking up functions just to keep them small is not necessary.  Of course you can consider doing so for other reasons
such as code reuse, but being small just for small’s sake seems unnecessary.

## I Prefer Integration Tests to Unit Tests

I am a huge fan of testing and highly recommend testing software as a key component of building maintainable systems.

htmx itself is only possible because we have a good [test suite](https://htmx.org/test) that helps us ensure
that the library stays stable as we work on it.

If you take a look at the [test suite](https://github.com/bigskysoftware/htmx/blob/master/test/core/ajax.js) one thing
you might notice is the relative lack of [Unit Tests](https://en.wikipedia.org/wiki/Unit_testing).  We have very few
test that directly call functions on the htmx object.  Instead, the tests are mostly
[Integration Tests](https://en.wikipedia.org/wiki/Integration_testing): they set up a particular DOM configuration
with some htmx attributes and then, for example, click a button and verify some things about the state of the DOM afterward.

This is in contrast with Clean Code’s recommendation of extensive *unit testing*, coupled with Test-First Development:

**First Law** You may not write production code until you have written a failing unit test.
**Second Law** You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
**Third** Law You may not write more production code than is sufficient to pass the currently failing test.

–[Clean Code](https://www.goodreads.com/book/show/3735293-clean-code)

I generally avoid doing this sort of thing, especially early on in projects.  Early on you often have no
idea what the right abstractions for your domain are, and you need to try a few different approaches to figure out what
you are doing.  If you adopt the test first approach you end up with a bunch of tests that are going to break as you
explore the problem space, trying to find the right abstractions.

Further, unit testing encourages the exhaustive testing of every single function you write, so you often end up having more
tests that are tied to a particular implementation of things, rather than the high level API or conceptual ideas of the
module of code.

Of course, you can and should refactor your tests as you change things, but the reality is that a large and growing test
suite takes on its own mass and momentum in a project, especially as other engineers join, making changes more and more
difficult as they are added.  You end up creating things like test helpers, mocks, etc. for your testing code.

All that code and complexity tends over time to lock you in to a particular implementation.

### Dirty Testing

My preferred approach in many projects is to do some unit testing, but not a ton, early on in the project and wait
until the core APIs and concepts of a module have crystallized.

At that point I then test the API exhaustively with integrations tests.

In my experience, these integration tests are much more useful than unit tests, because they remain stable and useful
even as you change the implementation around.  They aren’t as tied to the current codebase, but rather express higher level
invariants that survive refactors much more readily.

I have also found that once you have a few higher-level integration tests, you can then do Test-Driven development, but
at the higher level: you don’t think about units of code, but rather the API you want to achieve, write the tests for that
API and then implement it however you see fit.

So, I think you should hold off on committing to a large test suite until later in the project, and that test suite
should be done at a higher level than Test-First Development suggests.

Generally, if I can write a higher-level integration test
to demonstrate a bug or feature I will try to do so, with the hope that the higher-level test will have a longer shelf
life for the project.

## I Prefer To Minimize Classes

A final coding strategy that I use is that I generally strive to minimize the number of classes/interfaces/concepts in
my projects.

Clean Code does not explicitly say that you should maximize the # of classes in your system, but many recommendations it
makes tend to lead to this outcome:

- “Prefer Polymorphism to If/Else or Switch/Case”

- “The first rule of classes is that they should be small. The second rule of classes is that they should be smaller
than that.”

- “The Single Responsibility Principle (SRP) states that a class or module should have one, and only one, reason to
change.”

- “The first thing you might notice is that the program got a lot longer. It went from a little over one page to
nearly three pages in length.”

As with functions, I don’t think classes should be particularly small, or that you should prefer polymorphism to a
simple (or even a long, janky) if/else statement, or that a given module or class should only have one reason to change.

And I think the last sentence here is a good hint why: you tend to end up with a lot more code which may be of little
real benefit to the system.

### “God” Objects

You will often hear people criticise the idea of [“God objects”](https://en.wikipedia.org/wiki/God_object) and I
can of course understand where this criticism comes from: an incoherent class or module with a morass of unrelated
functions is obviously a bad thing.

However, I think that fear of “God objects” can tend to lead to an opposite problem: overly-decomposed software.

To balance out this fear, let’s look at one of my favorite software packages,
[Active Record](https://guides.rubyonrails.org/active_record_basics.html).

Active Record provides a way for you to map ruby object to a database, it is what is called an
[Object/Relational Mapping](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) tool.

And it does a great job of that, in my opinion: it makes the easy stuff easy,
the medium stuff easy enough, and when push comes to shove you can kick out to raw SQL without much fuss.

(This is a great example of what I call [“layering”](https://grugbrain.dev/#grug-on-apis) an API.)

But that’s not all the Active Record objects are good at: they also provide excellent functionality for building HTML
in the [view layer](https://guides.rubyonrails.org/action_view_overview.html) of Rails.  They don’t include *HTML specific* functionality, but they do offer functionality
that is useful on the view side, such as providing an API to retrieve error messages, even at the field level.

When you are writing Ruby on Rails applications you simply pass your Active Record instances out to the view/templates.

Compare this with a more heavily factored implementation, where validation errors are handled as their own “concern”.
Now you need to pass (or at least access) two different things in order to properly generate your HTML.  It’s not
uncommon in the Java community to adopt the [DTO](https://www.baeldung.com/java-dto-pattern) pattern and have another set of objects entirely
distinct from the ORM layer that is passed out to the view.

I like the Active Record approach.  It may not be [separating concerns](https://en.wikipedia.org/wiki/Separation_of_concerns)
when looked at from a purist perspective, but *my* concern is often getting data from a database into an HTML document,
and Active Record does that job admirably without me needing to deal with a bunch of other objects along the way.

This helps me minimize the total number of objects I need to deal with in the system.

Will some functionality creep into a model that is maybe a bit “view” flavored?

Sure, but that’s not the end of the world, and it reduces the number
of layers and concepts I have to deal with.  Having one class that handles retrieving data from the database, holding
domain logic and serves as a vessel for presenting information to the view layer simplifies things tremendously for me.

## Conclusion

I’ve given three examples of my codin’ dirty approach:

- I think (some) big functions are good, actually

- I prefer integration tests to unit tests

- I like to keep my class/interface/concept count down

I’m presenting this, again, not to convince *you* to code the way *I* code, or to suggest that the way I code is “optimal” in
any way.

Rather it is to give you, and especially you younger developers out there, a sense that you don’t *have* to write code
the way that many thought leaders suggest in order to have a successful software career.

You shouldn’t be intimidated if someone calls your code “dirty”: lots of very successful software has been written that
way and, if you focus on the [core ideas](https://en.wikipedia.org/wiki/Algorithms_%2B_Data_Structures_%3D_Programs)
of [software engineering](https://www.web.stanford.edu/~ouster/cgi-bin/book.php), you will likely be successful
in spite of how “dirty” it is, and maybe even because of it!
</Release>

<Release date="November 13, 2024" published="2024-11-13T00:00:00.000Z" url="https://htmx.org/essays/webcomponents-work-great/">
## Web Components Work Great with htmx

People interested in htmx often ask us about component libraries.
React and other JavaScript frameworks have great ecosystems of pre-built components that can be imported into your project; htmx doesn’t really have anything similar.

The first and most important thing to understand is that htmx doesn’t preclude you from using *anything*.
Because htmx-based websites are [often multi-page apps](https://unplannedobsolescence.com/blog/less-htmx-is-more/), each page is a blank canvas on which you can import as much or as little JavaScript as you like.
If your app is largely hypermedia, but you want an interactive, React-based calendar for one page, just import it on that one page with a script tag.

We sometimes call this pattern “Islands of Interactivity”—it’s referenced in our explainers [here](https://htmx.org/essays/10-tips-for-ssr-hda-apps/#tip-8-when-necessary-create-islands-of-interactivity), [here](https://htmx.org/essays/hypermedia-friendly-scripting/#islands), and [here](https://htmx.org/essays/you-cant/#myth-5-with-htmx-or-mpas-every-user-action-must-happen-on-the-server).
Unlike JS frameworks, which are largely incompatible with each other, using islands with htmx won’t lock you into any specific paradigm.

But there’s a second way that you can re-use complex frontend functionality with htmx, and it’s [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components)!

## Practical Example

Let’s say that you have a table that says what carnival rides everyone is signed up for:

  
    Name
    Carousel
    Roller Coaster
  
  
    Alex
    Yes
    No
  
  
    Sophia
    Yes
    Yes
  

Alex is willing to go on the carousel but not the roller coaster, because he is scared; Sophia is not scared of either.

I built this as a regular HTML table ([closing tags are omitted](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td#technical_summary) for clarity):

```
table>
  tr>th>Name    th>Carousel  th>Roller Coaster
  tr>td>Alex    td>Yes       td>No
  tr>td>Sophia  td>Yes       td>Yes
table>

```

Now imagine we want to make those rows editable.
This is a classic situation in which people reach for frameworks, but can we do it with hypermedia?
Sure!
Here’s a naive idea:

```
form hx-put=/carnival>
table>
  tr>
    th>Name
    th>Carousel
    th>Roller Coaster
  tr>
  tr>
    td>Alex
    td>select name="alex-carousel"> option selected>Yes option>No option> Maybeselect>
    td>select name="alex-roller"> option>Yes option selected>No option> Maybeselect>
  tr>
  tr>
    td>Sophia
    td>select name="sophia-carousel"> option selected>Yes option>No option> Maybeselect>
    td>select name="sophia-roller"> option selected>Yes option>No option> Maybeselect>
  tr>
table>
button>Savebutton>
form>

```

That will give us this table:

  
    Name
    Carousel
    Roller Coaster
  
  
    Alex
    
    
  
  
    Sophia
    
    
  

Save

That’s not too bad!
The save button will submit all the data in the table, and the server will respond with a new table that reflects the updated state.
We can also use CSS to make the ``s fit our design language.
But it’s easy to see how this could start to get unwieldy—with more columns, more rows, and more options in each cell, sending all that information each time starts to get costly.

Let’s remove all that redundancy with a web component!

```
form hx-put=/carnival>
table>
  tr>
    th>Name
    th>Carousel
    th>Roller Coaster
  tr>
  tr>
    td>Alex
    td>edit-cell name="alex-carousel" value="Yes">edit-cell>
    td>edit-cell name="alex-roller" value="No">edit-cell>
  tr>
  tr>
    td>Sophia
    td>edit-cell name="sophia-carousel" value="Yes">edit-cell>
    td>edit-cell name="sophia-roller" value="Yes">edit-cell>
  tr>
table>
button>Savebutton>
form>

```

We still have an entirely declarative [HATEOAS](https://htmx.org/essays/hateoas/) interface—both current state (the `value` attribute) and possible actions on that state (the `` and `` elements) are efficiently encoded in the hypertext—only now we’ve expressed the same ideas a lot more concisely.
htmx can add or remove rows (or better yet, whole tables) with the `` web component as if `` were a built-in HTML element.

You’ve probably noticed that I didn’t include the implementation details for `` (although you can, [of course](https://htmx.org/essays/right-click-view-source/), View Source this page to see them).
That’s because they don’t matter!
Whether the web component was written by you, or a teammate, or a library author, it can be used exactly like a built-in HTML element and htmx will handle it just fine.

## Don’t Web Components have some problems?

A lot of the problems that JavaScript frameworks have supporting Web Components don’t apply to htmx.

Web Components [have DOM-based lifecycles](https://dev.to/ryansolid/web-components-are-not-the-future-48bh), so they are difficult for JavaScript frameworks, which often manipulate elements outside of the DOM, to work with.
Frameworks have to account for some [bizarre and arguably buggy](https://x.com/Rich_Harris/status/1841467510194843982) APIs that behave differently for native DOM elements than they do for custom ones.
Here at htmx, we agree with [SvelteJS creator Rich Harris](https://x.com/Rich_Harris/status/1839484645194277111): “web components are [not] useful primitives on which to build web frameworks.”

The good news is that htmx [is not really a JavaScript web framework](https://htmx.org/essays/is-htmx-another-javascript-framework/).
The DOM-based lifecycles of custom elements work great in htmx, because everything in htmx has a DOM-based lifecycle—we get stuff from the server, and we add it to the DOM.
The default htmx swap style is to just set [`.innerHTML`](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML), and that works great for the vast majority of users.

That’s not to say that htmx doesn’t have to accommodate weird Web Component edge cases.
Our community member and resident WC expert [Katrina Scialdone](https://unmodernweb.com/) merged [Shadow DOM support for htmx 2.0](https://github.com/bigskysoftware/htmx/pull/2075), which lets htmx process the implementation details of a Web Component,
and supporting that is [occasionally](https://github.com/bigskysoftware/htmx/pull/2846) [frustrating](https://github.com/bigskysoftware/htmx/pull/2866).
But being able to work with both the [Shadow DOM](https://htmx.org/examples/web-components/) and the [“Light DOM”](https://meyerweb.com/eric/thoughts/2023/11/01/blinded-by-the-light-dom/) is a nice feature for htmx, and it carries a relatively minimal support burden because htmx just isn’t doing all that much.

## Bringing Behavior Back to the HTML

A couple of years ago, W3C Contributor (and Web Component proponent, I think) Lea Verou wrote the following, in a blog post about [“The failed promise of Web Components”](https://lea.verou.me/blog/2020/09/the-failed-promise-of-web-components/):

the main problem is that HTML is not treated with the appropriate respect in the design of these components. They are not designed as closely as possible to standard HTML elements, but expect JS to be written for them to do anything. HTML is simply treated as a shorthand, or worse, as merely a marker to indicate where the element goes in the DOM, with all parameters passed in via JS.

Lea is identifying an issue that, from the perspective of 2020, would have seemed impossible to solve: the cutting-edge web developers targeted by Web Components were not writing HTML, they were writing JSX, usually with React (or Vue, or what have you).
The idea that [behavior belongs in the HTML](https://unplannedobsolescence.com/blog/behavior-belongs-in-html/) was, in the zeitgeist, considered [a violation of separation of concerns](https://htmx.org/essays/locality-of-behaviour/);
disrespecting HTML was best practice.

The relatively recent success of htmx—itself now a participant in the zeitgeist—offers an alternative path: take HTML seriously again.
If your website is one whose functionality can be primarily described with [large-grain hypermedia transfers](https://htmx.org/essays/when-to-use-hypermedia/) (we believe most of them can), then the value of being able to express more complex patterns through hypermedia increases dramatically.
As more developers use htmx (and multi-page architectures generally) to structure their websites,
perhaps the demand for Web Components will increase along with it.

Do Web Components “just work” everywhere? Maybe, maybe not. But they do work here.

class EditCell extends HTMLElement {
  connectedCallback() {
    this.value = this.getAttribute("value")
    this.name = this.getAttribute("name")

    this.innerHTML = `
      
        Yes
        No
        Maybe
      
    `
  }
}

customElements.define('edit-cell', EditCell)
</Release>

<Release date="November 7, 2024" published="2024-11-07T00:00:00.000Z" url="https://htmx.org/essays/a-real-world-nextjs-to-htmx-port/">
## Next.js to htmx — A Real World Example

Over 6 years ago, I created [an open source URL shortener](https://github.com/thedevs-network/kutt) with Next.js and after years of working on it, I found Next.js to be much more of a burden than a help. Over the years, Next.js has changed, and so did my code so it can be compatible with those changes.

My Next.js codebase grew bigger, and its complexity increased by greater size. I had dozens of components and a list of dependencies to manage. I ended up maintaining the code constantly just to keep it alive. Sure, Next.js helped here and there, but at what cost?

I asked myself, what am I doing on my website that is so complex that needs all that JavaScript code to decide what to render and how to render on my webpage? Next.js was trying to render the webpage from the server side, so why won’t I send the HTML directly myself?

So I decided to try a new route—some might say the good ol’ route—and choose plain HTML and use the help of htmx for that.

## Video

Watch me go full in details here:

[Video](https://www.youtube.com/watch?v=8RL4NvYZDT4)

## The process

Replacing my components with the equivalent HTML elements powered by htmx wasn’t exactly an easy task, but one that was worth the time. I had to view things from a different angle, and I sometimes felt strict in what user interactions I can implement, but what I created was reliable and fast.

All the build steps were gone; no more transpiling and compiling the code. What you see is what you get. Most of the dependencies became redundant and have been removed. All the main logic of the website was moved to the server side, holding one source for the truth.

In the Next.js version I had isolated components, global states, and all that JavaScript to handle the forms or update the content, and yet, everything was more intuitive with htmx. After trying it, sending and receiving HTML suddenly made sense.

## Summary

- Dependencies are **reduced by 87%** (**24** to **3**!)

- I wrote **less code by 17%**  (**9500 LOC** to **7900 LOC**.) In reality the total LOC of the code base is **reduced by more than 50%**, since much less code is imported from the dependencies.

- Web build time was **reduced by 100%** (there’s **no build step** anymore.)

- Size of the website **reduced by more than 85%** (**~800KB** to **~100KB**!)

These numbers signify a great improvement, however, what is important for me at the end is the user and the developer experience, which to me htmx won at both.
</Release>

<Release date="September 30, 2024" published="2024-09-30T00:00:00.000Z" url="https://htmx.org/essays/why-gumroad-didnt-choose-htmx/">
## Why Gumroad Didn't Choose htmx

At Gumroad, we recently embarked on a new project called [Helper](https://helper.ai). As the CEO, I was initially quite
optimistic about using [htmx](https://htmx.org) for this project, even though some team members were less enthusiastic.

My optimism stemmed from previous experiences with React, which often felt like overkill for our needs. I thought htmx
could be a good solution to keep our front-end super light.

[
![GitHub screenshot shows deleted files](/img/gumroad-red.jpeg)
](/img/gumroad-red.jpeg) 
Source with htmx - Click Image To View

In fact, I shared this sentiment with our team in Slack:

“https://htmx.org/ may be a way of adding simple interactions to start”

And initially, it seemed promising! As one of our engineers at Gumroad eloquently put it:

“HTMX is (officially) a meme to make fun of how overly complicated the JS landscape has gotten - much like tailwind is
just a different syntax for inline CSS, HTMX is a different syntax for inline JS.”

However, unlike Tailwind, which has found its place in our toolkit, htmx didn’t scale for our purposes and didn’t lead
to the best user experience for our customers–at least for our use case.

Here’s why:

- 

**Intuition and Developer Experience**: While it would have been possible to do the right thing in htmx, we found it
much more intuitive and fun to get everything working with Next.js. The development process felt natural with
Next.js, whereas with htmx, it often felt unnatural and forced. For example, when building complex forms with dynamic
validation and conditional fields, we found ourselves writing convoluted server-side logic to handle what would be
straightforward client-side operations in React.

- 

**UX Limitations**: htmx ended up pushing our app towards a Rails/CRUD approach, which led to a really poor (or at
least, boring and generic) user experience by default. We found ourselves constantly fighting against this tendency,
which was counterproductive. For instance, implementing a drag-and-drop interface for our workflow builder proved to
be a significant challenge with htmx, requiring workarounds that felt clunky compared to the smooth experience we
could achieve with React libraries.

- 

**AI and Tooling Support**: It’s worth noting that AI tools are intimately familiar with Next.js and not so much with
htmx, due to the lack of open-source training data. This is similar to the issue Rails faces. While not a
dealbreaker, it did impact our development speed and the ease of finding solutions to problems. When we encountered
issues, the wealth of resources available for React/Next.js made troubleshooting much faster.

- 

**Scalability Concerns**: As our project grew in complexity, we found htmx struggling to keep up with our needs. The
simplicity that initially attracted us began to feel limiting as we tried to implement more sophisticated
interactions and state management. For example, as we added features like real-time collaboration and complex data
visualization, managing state across multiple components became increasingly difficult with htmx’s server-centric
approach.

- 

**Community and Ecosystem**: The React/Next.js ecosystem is vast and mature, offering solutions to almost any problem
we encountered. With htmx, we often found ourselves reinventing the wheel or compromising on functionality. This
became particularly evident when we needed to integrate third-party services and libraries, which often had React
bindings but no htmx equivalents.

[
![GitHub: 1 added file](/img/gumroad-green.jpeg)
](/img/gumroad-green.jpeg) 
Source with Next.js - Click Image To View

Ultimately, we ended up moving to React/Next.js, which has been a really great fit for building the complex UX we’ve
been looking for. We’re happy with this decision–for now. It’s allowed us to move faster, create more engaging user
experiences, and leverage a wealth of existing tools and libraries.

[
![The old Gumroad Helper interface is quite basic with a single flat form,
  while the new one has multiple levels of navigation and editable lists.](/img/gumroad-helper-before-after.png)
](/img/gumroad-helper-before-after.png) 
Gumroad Helper Before & After - Click Image To View

This experience has reinforced a valuable lesson: while it’s important to consider lightweight alternatives, it’s
equally crucial to choose technologies that can grow with your project and support your long-term vision. For Helper,
React and Next.js have proven to be that choice.

Since we’ve moved there, we’ve been able to seriously upgrade our app’s user experience for our core customers.

- 

**Drag-and-Drop Functionality**: One of the key features of our workflow builder is the ability to reorder steps
through drag-and-drop. While it’s possible to implement drag-and-drop with htmx, we found that the available
solutions felt clunky and required significant custom JavaScript. In contrast, React ecosystem offers libraries like
react-beautiful-dnd that provide smooth, accessible drag-and-drop with minimal setup.

- 

**Complex State Management**: Each workflow step has its own set of configurations and conditional logic. As users
edit these, we need to update the UI in real-time to reflect changes and their implications on other steps. With
htmx, this would require numerous server roundtrips or complex client-side state management that goes against htmx’s
server-centric philosophy. React’s state management solutions (like useState or more advanced options like Redux)
made this much more straightforward.

- 

**Dynamic Form Generation**: The configuration for each step type is different and can change based on user input.
Generating these dynamic forms and handling their state was more intuitive with React’s component model. With htmx,
we found ourselves writing more complex server-side logic to generate and validate these forms.

- 

**Real-time Collaboration**: While not visible in this screenshot, we implemented features allowing multiple users to
edit a workflow simultaneously. Implementing this with WebSockets and React was relatively straightforward, whereas
with htmx, it would have required more complex server-side logic and custom JavaScript to handle real-time updates.

- 

**Performance Optimization**: As workflows grew larger and more complex, we needed fine-grained control over
rendering optimizations. React’s virtual DOM and hooks like useMemo and useCallback allowed us to optimize
performance in ways that weren’t as readily available or intuitive with htmx.

It’s important to note that while these challenges aren’t insurmountable with htmx, we found that addressing them often
led us away from htmx’s strengths and towards solutions that felt more natural in a JavaScript-heavy environment. This
realization was a key factor in our decision to switch to React and Next.js.

We acknowledge that htmx may be a great fit for many projects, especially those with simpler interaction models or those
built on top of existing server-rendered applications. Our experience doesn’t invalidate the benefits others have found
in htmx. The key is understanding your project’s specific needs and choosing the tool that best aligns with those
requirements.

In our case, the complex, stateful nature of Helper’s interface made React and Next.js a better fit. However, we
continue to appreciate htmx’s approach and may consider it for future projects where its strengths align better with our
needs.

That said, we’re always open to reevaluating our tech stack as our needs evolve and new technologies emerge. Who knows
what the future might bring?
</Release>

<Pagination cursor="2024-09-30T00:00:00.000Z|2026-05-05T13:23:57.963Z|rel_kH-X4vNI2a8mrNs2sy36R" next="https://releases.sh/htmx/htmx.md?cursor=2024-09-30T00%3A00%3A00.000Z%7C2026-05-05T13%3A23%3A57.963Z%7Crel_kH-X4vNI2a8mrNs2sy36R&limit=20" />
