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.