In my previous article, I introduced you to building an HTTP server with Express.js. Now we’ll continue building on that example with a foray into more advanced use cases: a quick guide to using Express view templates and templating engines, persisting data with Express, and how to use Express with HTMX.
Templates in Express
View templates let you define the response output of an endpoint using HTML that has access to variables and logic. A template lets you write familiar HTML-like files that can access live data within an application. Templating is a time-tested approach to the server-side generation of views and Express supports many templating technologies.
One of the most popular templating engines in the JavaScript world is Pug. Here’s an example of a simple Pug template:
html
body
h1= quote
Picking up our sample application from the previous article, let’s save this template in a /views/index.pug
file, then add the pug
package to our app:
npm install pug
Now we can open our server.js
and add the pug
plugin as our view engine:
app.set('view engine', 'pug')
Then we add a mapping to the index.pug
file we just created:
app.get('/', (req, res) => {
res.render('index', { quote: 'That's no moon!' })
})
In the response to /
, we create a property called index
, which binds the associated data to the index template. The value of the property is a JSON object literal, with the quote
property we used in the index.pug
file.
When we visit localhost:3000/
, we now get the following HTML response:
That's no moon!
Pug and similar templating tools are examples of domain-specific languages, or DSLs. Pug takes all the formalism of HTML and boils it down to a simple syntax. Our example also demonstrates Pug’s ability to reach into the live data of our JavaScript code and use the variables there. These two simple features open up a huge range of power. Although reactive front ends like React and Angular are popular, using something like Pug with Express can do everything you need with a simpler setup, faster initial load times, and inherent SEO-friendliness. Benefits like these are the reason many reactive frameworks use server-side generation as part of their template engine.
You can learn more about Pug’s syntax here and more about its interaction with Express here. We’ll also see more of Pug in action soon.
Persistent data with Express
Now let’s think about how to incorporate data persistence into our Express application. Persistence is an essential aspect of data-driven websites and applications and there are many different databases available to handle it, from SQL stores like Postgres to NoSQL options like MongoDB.
The recent integration of SQLite directly into Node.js makes it a very low-effort option, which is especially tempting for prototyping. You can learn more about the overall benefits and characteristics of SQLite in this article.
You don’t need to add SQLite with NPM if you are using the latest version of Node, though you may have to use the --experimental-sqlite
flag depending on your Node version. You can import SQLite like so:
const DatabaseSync = require("node:sqlite").DatabaseSync;
const database = new DatabaseSync('db.sql');
And then create a database:
database.exec(`
CREATE TABLE IF NOT EXISTS quotes (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, quote TEXT)
`);
We could insert some test rows like so:
insert.run('Obi Wan Kenobi', "That’s no moon!" );
insert.run('The Terminator', 'Hasta la vista, baby.');
insert.run("Captain Ahab", "There she blows!");
Now let’s create a way to make new quotes. First, we’ll need an endpoint that accepts requests with the data for new quotes, in server.js
:
app.post('/quotes', (req, res) => {
const { quote, author } = req.body;
const insert = database.prepare('INSERT INTO quotes (name, quote) VALUES (?, ?)');
insert.run(author, quote);
res.redirect('/');
});
This tells Express to accept requests at /quotes
, then parses and destructures the request body into two variables: quote
and author
. We use these two variables in a prepared statement that inserts a new row into the quotes
database. After that, we redirect to the homepage, where the list of quotes (which we’ll build in a moment) can render the new item.
Update the views/index.pug
to include a form, like so:
h2 Add a New Quote
form(action="/quotes" method="post")
label(for="quote") Quote:
input(type="text" id="quote" name="quote" required)
br
label(for="author") Author:
input(type="text" id="author" name="author" required)
br
button(type="submit") Add Quote
This gives us a simple form for the user to submit the quote
and author
fields to the /quotes
endpoint. At this point, we can test the application by running it ($ node server.js
) and visiting localhost:3000
.
We still need a way to list the existing quotes. We can add an endpoint to provide the data like so:
// server.js
app.get('/', (req, res) => {
const query = database.prepare('SELECT * FROM quotes ORDER BY name');
let rows = [];
query.all().forEach((x) => {
console.log(x)
rows.push({ name: x.name, quote: x.quote });
res.render('index', { quotes: JSON.stringify(rows) });
});
});
Now, when we visit the root path at /
, we’ll first request all the existing quotes from the database and build a simple array of objects with quote
and name
fields on them. We pass that array on to the index
template as the quotes
property.
The index.pug
template then makes use of the quotes
variable to render an unordered list of items:
ul
each quote in quotes
li
p= quote.quote
span - #{quote.author}
Express with HTMX
At this point, we could think about making things look better with CSS or expanding the API. Instead, let’s make the form submission AJAX-driven with HTMX. Compared with a reactive front end, the form-submit-and-page-reload style of interaction is a bit clunky. HTMX should let us keep the basic layout of things but submit the form without a page reload (and keep our list updated).
The first thing we need to do is to add three HTMX attributes to the Pug template:
h2 Add a New Quote
form(hx-post="/quotes" hx-target="#quote-list" hx-swap="beforeend")
label(for="quote") Quote:
input(type="text" id="quote" name="quote" required)
br
label(for="author") Author:
input(type="text" id="author" name="author" required)
br
button(type="submit") Add Quote
Only the form tag itself has changed:
form(hx-post="/quotes" hx-target="#quote-list" hx-swap="beforeend")
The hx-post
syntax points to where to submit the AJAX request, which in this case is our server endpoint. The hx-target
says where to put the content coming back from the AJAX call. And hx-swap
says where in the target to put the content.
For this to work, we need to include HTMLX in the head:
head
title Quotes
script(src="https://unpkg.com/htmx.org@1.9.6")
On the server, in server.js
, our endpoint could look something like this:
app.post('/quotes', (req, res) => {
const { quote, author } = req.body;
const insert = database.prepare('INSERT INTO quotes (name, quote) VALUES (?, ?)');
insert.run(author, quote);
res.render('quote-item', { quote: quote, author: author });
});
Instead of rendering the main page at index.pug
, which we did previously, in this instance we return the markup for the newly created item. To do this, we use another template:
// views/quote-item.pug
li
p= quote
span - #{author}
If you run the application now, you’ll see that submitting the form doesn’t cause a page reload and the new item is dynamically inserted into the list. The difference in behavior is immediately obvious.
Conclusion
We haven’t written any front-end JavaScript but were able to use HTMX to submit the form in the background and seamlessly update the UI. HTMX has more dynamic UI capabilities up its sleeve, although it isn’t a replacement for the kind of reactive state machine you could build with React or Svelte.
The beauty of our setup so far is the simplicity married to a respectable amount of power. HTMX lets us painlessly adopt a more powerful HTML. For that reason, I wouldn’t be surprised to see it become part of the official HTML specification at some point.
On the server side, we no longer have a data endpoint. Now we have an endpoint that emits HTMX, which is an alternative way to go about the client-server to the present mainstream. If you’re looking for a comfortable middle ground, this kind of stack could be the thing.