Welcome to the second module of our course. In the previous module, we explored the "why" of HTMX—the hypermedia mental model, its architectural principles, and the trade-offs compared to client-side frameworks like React. We concluded that for many CRUD-style applications, the simplicity and performance gains of a server-rendered approach are compelling.
Now, we shift from theory to practice. This module focuses on building the server-side foundation that makes HTMX possible. Today's lesson is your first hands-on step: we will configure a complete, working Node.js server using the Express framework. You will learn to set up a templating engine (EJS) to generate HTML, configure middleware to serve static files like CSS, and, crucially, enable your server to process form submissions—a cornerstone of hypermedia interactions.
The Anatomy of an HTMX-Ready Server
Before we write any code, let's look at the architecture we're about to build. At its core, an Express server powering an HTMX application handles requests, processes data, and responds not with JSON, but with HTML.

To achieve this, our server needs three key capabilities, which we will configure today:
- Templating: The ability to dynamically generate HTML using a templating engine. We'll use EJS.
- Static Asset Serving: The ability to serve static files like CSS stylesheets, images, and the HTMX library itself.
- Request Body Parsing: The ability to understand data sent from the browser, particularly from HTML forms, which HTMX uses extensively.
Step 1: Project Initialization and Structure
A well-structured project is the foundation of a maintainable application. For an Express and HTMX project, a common convention is to separate server logic, templates (views), and public assets.
The image below shows a typical file structure. Notice the distinct views and public directories, which we are about to create.

Let's begin by setting up this structure. The following article provides a concise guide to the initial setup.
htmx and Alpine.js: Build Interactive Web Apps Without Heavy JavaScript Frameworks
This tutorial from noqta.tn provides a clear, step-by-step guide for initializing the project and installing the necessary packages.
Follow the instructions in the section "Step 2: Project Setup". Create your project directory, initialize npm and install Express and EJS. Then, create the recommended directory structure using the mkdir command provided. Stop before "Step 3".
After completing these steps, you should have a package.json file and empty views and public directories. You have now laid the groundwork for our server.
Step 2: Configuring the Express Application
With the project structure in place, we will now create our server.js file and configure the Express application. This involves telling Express how to handle templates, static files, and incoming data.
For a dynamic overview of this process, the following video by Brad Traversy provides an excellent, fast-paced walkthrough of setting up a basic Express server.
HTMX Crash Course | Dynamic Pages Without Writing Any JavaScript
This segment from the Traversy Media crash course demonstrates the essential middleware setup for an Express server. It's a great visual complement to the code we will write next.
Watch the section from the server setup. Pay close attention to his explanation of express.static and the middleware for parsing request bodies. Note that he uses express.urlencoded and express.json, which are the modern, built-in replacements for the older body-parser package.
Now, let's implement this in our own server.js. The article we used for setup provides the exact code we need.
htmx and Alpine.js: Build Interactive Web Apps Without Heavy JavaScript Frameworks
This section contains the full server.js file. We will focus on the initial configuration lines.
In the article, find the code block under "Step 3: Setting Up the Express Server". You will create your own server.js file and add the configuration code. For now, focus on understanding these specific lines: the configuration section. You can ignore the in-memory task store and the routes for now.
Let's break down those crucial configuration lines:
-
app.set("view engine", "ejs");
This line registers EJS as the application's view engine. When you later callres.render('some-template'), Express will automatically look forsome-template.ejsinside theviewsdirectory and use the EJS engine to process it. -
app.use(express.static("public"));
This configures the static file server. It tells Express that any request for a file that is not handled by a specific route should be looked for inside thepublicdirectory. This is how you will serve your CSS, images, and any client-side JavaScript. -
app.use(express.urlencoded({ extended: true }));
This is the modern equivalent of thebody-parsermiddleware. It parses incoming requests withapplication/x-www-form-urlencodedpayloads, which is the default encoding for HTML forms. This middleware takes the form data, parses it, and attaches it as a JavaScript object torequest.body, making it easily accessible in your route handlers. This is absolutely essential for handling data from HTMXhx-postrequests.
Some tutorials, like the one in the LogRocket article Creating server-driven web apps with htmx, still mention installing body-parser as a separate package. While that still works, using the built-in express.urlencoded is the current best practice.
Step 3: Rendering the First Page
Our server is configured. The final step is to create a route that renders a view, and a view that includes the HTMX library.
First, let's create our main template file, views/index.ejs. This file will contain our basic HTML structure and a script tag to load HTMX from a CDN.
htmx and Alpine.js: Build Interactive Web Apps Without Heavy JavaScript Frameworks
This section shows the main layout file, views/index.ejs, which is exactly what we need for our first page.
Find the section "Step 4: The Main Layout with htmx and Alpine.js". Create a file named index.ejs inside your views folder. Copy the HTML content from the provided code block into your file. Notice the <script src="https://unpkg.com/htmx.org@..."> tag in the <head>—this is how we add HTMX to our page.
Now, we need to add the code to server.js to render this template and start the server.
Let's add a root route handler and the app.listen call to your server.js file. Your complete server.js should now look something like this, combining the configuration with a simple route and the server start command:
const express = require("express");
const app = express();
// 1. Set the view engine to EJS
app.set("view engine", "ejs");
// 2. Serve static files from the 'public' folder
app.use(express.static("public"));
// 3. Parse URL-encoded bodies (as sent by HTML forms)
app.use(express.urlencoded({ extended: true }));
// Main page route
app.get("/", (req, res) => {
// The 'tasks' variable is passed to the template.
// For now, it's just an empty array.
res.render("index", { tasks: [] });
});
// Start the server
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
You can now run your server from the terminal with node server.js. When you navigate to http://localhost:3000 in your browser, Express will execute your / route handler, render the index.ejs template, and serve it. The browser will then fetch the HTMX library from the CDN link in the <head> section. You have successfully set up a hypermedia-ready server.
Conclusion
In this lesson, you have moved from theory to practice, building the essential server-side foundation for your HTMX applications. You've configured a complete Express server from scratch, a critical skill for this migration journey.
The key takeaways are:
- Core Configuration: An HTMX-ready Express server requires three key pieces of middleware: the view engine (
app.set), the static file server (express.static), and the request body parser (express.urlencoded). - Project Structure: A clean separation of concerns using
viewsfor templates andpublicfor static assets is a standard and effective practice. - Templating is Key: The server's primary job in a hypermedia architecture is to render HTML.
res.render()is the command that makes this happen.
You now have a server that can render a full HTML page. In our next lesson, we will make it more powerful by designing routes that can respond with either a full page or a small HTML fragment, depending on whether the request comes from a user's initial visit or an HTMX-powered interaction. This is the core mechanic that enables dynamic, partial page updates.
Can't find a good explanation? Sign up and we'll make it for you
Sign up