Matthew Tyson
Contributing Writer

Full-stack web development with HTMX and Bun, Part 1: Elysia and MongoDB

how-to
Apr 3, 20248 mins
Development Libraries and FrameworksJavaScriptWeb Development

Put HTMX on the front end and Bun on the back, then hook them up with Elysia and MongoDB for an agile tech stack that makes developing web apps a breeze.

Surfer rides a wave
Credit: EpicStockMedia/Shutterstock

Bun and HTMX are two of the most interesting things happening in software right now. Bun is an incredibly fast, all-in-one server-side JavaScript platform, and HTMX is an HTML extension used to create simple, powerful interfaces. In this article, weโ€™ll use these two great tools together to develop a full-stack application that uses MongoDB for data storage and Elysia as its HTTP server.

The tech stack

Our focus in this article is how the four main components of our tech stack interact. The components are Bun, HTMX, Elysia, and MongoDB.ย This stack gives you a fast-moving setup that isย easy to configure and agile to change.ย 

  • Bun is a JavaScript runtime, bundler, package manager, and test runner.
  • Elysia is a high-performance HTTP server that is similar to Express but built for Bun.
  • HTMX offers a novel approach to adding fine-grained interactivity to HTML.
  • MongoDB is the flagship NoSQL document-oriented datastore.

Note that this article has two parts. In the second half, we will incorporate Pug, the HTMX templating engine, which weโ€™ll use to develop a few fancy front-end interactions.

Installation and set up

Youโ€™ll need to install Bun.js, which is easy to do.ย Weโ€™re also going to run MongoDB as a service alongside Bun on our development machine. You can read about installing and setting up MongoDB here.ย Once you have these packages installed, both the bun -v and mongod -version commands should work from the command line.

Next, letโ€™s begin a new project:


$ bun create elysia iw-beh

This tells bun to create a new project using the Elysia template.ย A template in Bun is a convenient way to jumpstart projects using the create command.ย Bun can work like Node, without any configuration, but the config is nice to have.ย (Learn more about Bun templates here.)

Now, move into the new directory: $ cd iw-beh.

And run the project as it is: $ bun run src/index.js.

This last command tells bun to run the src/index.js file. The src/index.js file is the code to start a simple Elysia server:


import { Elysia } from "elysia";

const app = new Elysia()
  .get("/", () => "Hello Elysia")
  .listen(3000);

console.log(
  `๐ŸฆŠ Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);

In this file, we import Elysia and use it to instantiate a new server that listens on port 3000 and has a single GET endpoint at the root.ย This endpoint returns a text string: โ€œHello Elysia.โ€ย How all this works is similar in spirit to Express.

If you visit localhost:3000 youโ€™ll get a simple greeting:

A simple greeting that says: 'Hello, Elysia' IDG

Now that we have Elysia running, letโ€™s add the static plugin.ย Elysia has several plugins for handling common scenarios.ย In this case, we want to serve some HTML from disk.ย The static plugin is just what we need:


$ bun add @elysiajs/static

Now the Elysia server running the static plugin should serve everything in the iw-beh/public directory.ย If we drop a simple HTML file in there and visit localhost:3000/ public, weโ€™ll see its contents.

The magic of HTMX

Next, letโ€™s add an HTMX page to index.html.ย Hereโ€™s a simple one from the HTMX homepage:


<script src="https://unpkg.com/htmx.org@1.9.10"></script>

<button hx-post="/clicked"
    hx-trigger="click"
    hx-target="#parent-div"
    hx-swap="outerHTML">
    Click Me!
</button>

This page displays a button. When clicked, the button makes a call to the server for the /clicked endpoint, and the button is replaced with whatever is in the response.ย Thereโ€™s nothing there yet, so it currently doesnโ€™t do anything.

But notice that all this is still HTML.ย We are making an API call and performing a fine-grained DOM change without any JavaScript.ย (The work is being done by the JavaScript in the htmx.org library we just imported, but the point is we donโ€™t have to worry about it.)

HTMX provides an HTML syntax that does these things using just three element attributes:

  • hx-post submits a post when it is triggered for an AJAX request.
  • hx-target tells hx-post which events execute an AJAX request.
  • hx-swap says what to do when an AJAX event occurs. In this case, replace the present element with the response.

Thatโ€™s pretty simple!

Elysia and MongoDB

Now letโ€™s make an endpoint in Elysia that will write something to MongoDB.ย First, weโ€™ll add the MongoDB driver for Bun:


bun add mongodb

Next, modify src.index.ts like this:


import { Elysia } from "elysia";
import { staticPlugin } from '@elysiajs/static';
const { MongoClient } = require('mongodb');

const app = new Elysia()
  .get("/", () => "Hello Elysia")
  .get("/db", async () => {

    const url = "mongodb://127.0.0.1:27017/quote?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.8.0";

    const client = new MongoClient(url, { useUnifiedTopology: true });
try {

    await client.connect();

    const database = client.db('quote');
    const collection = database.collection('quotes');

    const stringData = "Thought is the grandchild of ignorance.";

    const result = await collection.insertOne({"quote":stringData});
    console.log(`String inserted with ID: ${result.insertedId}`);

  } catch (error) {
    console.error(error);
  } finally {
    await client.close();
  }
          return "OK";
  })
  .use(staticPlugin())
  .listen(3000);

console.log(
  `๐ŸฆŠ Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);

In this code, weโ€™ve added a /db endpoint that talks to MongoDB.ย Right now, it just writes a quote to the quote database, inside the quotes collection. You can test this directly by going to localhost:3000/db.ย Then, you can verify the data is in MongoDB:


$ mongosh

test> use quote
switched to db quote
quote> db.quotes.find()
[
  {
    _id: ObjectId("65ba936fd59e9c265cc8c092"),
    quote: 'Thought is the grandchild of ignorance.',
    author: 'Swami Venkatesananda'
  }
]

Create an HTMX table

Now that we are connecting to the database from the front end, letโ€™s create a table to output the existing quotes.ย As a quick test, weโ€™ll add an endpoint to the server:


.get("/quotes", async () => {
    const data = [
      { name: 'Alice' },
      { name: 'Bob' },
    ];
    // Build the HTML list structure
  let html = '<ul>';
  for (const item of data) {
    html += `<li>${item.name}</li>`;
  }
  html += '</ul>';

    return html
  })

And then use it in our index.html:


<ul id="data-list"></ul>
<button hx-get="/quotes" hx-target="#data-list">Load Data</button>

Now, when you load /public/index.html and click the button, the list sent from the server is displayed.ย Notice that issuing HTML from the endpoint is different from the common JSON pattern.ย That is by design.ย We are conforming to RESTful principles here. HTMX has plugins for working with JSON endpoints, but this is more idiomatic.

In our endpoint, we are just manually creating the HTML.ย In a real application, weโ€™d probably use some kind of JavaScript-HTML templating framework to make things more manageable.

Now, we can retrieve the data from the database:


.get("/quotes", async () => {

const url = "mongodb://127.0.0.1:27017/quote?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.8.0";

      const client = new MongoClient(url, { useUnifiedTopology: true });
    try {
      await client.connect();

      const database = client.db('quote');
      const collection = database.collection('quotes');

      const quotes = await collection.find().toArray();

      // Build the HTML table structure
      let html = '<table class="legacyTable" border="1">';
      html += '<tr><th>Quote</th><th>Author</th></tr>';
      for (const quote of quotes) {
        html += `<tr><td>${quote.quote}</td><td>${quote.author}</td></tr>`;
      }
      html += '</table>';

      return html;
    } catch (error) {
      console.error(error);
      return "Error fetching quotes";
    } finally {
      await client.close();
    }

  })

In this endpoint, we retrieve all the existing quotes in the database and return them as a simple HTML table. (Note that in a real application, weโ€™d extract the database connection work to a central place.)

Youโ€™ll see something like this:

HTMX table with one row and text. IDG

This screenshot shows the one row we inserted when we hit the /db endpoint earlier.

Now, letโ€™s add the ability to create a new quote.ย Hereโ€™s the back-end code (src/index.ts):


app.post("/add-quote", async (req) => {
    const url = "mongodb://127.0.0.1:27017/quote?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.8.0";

    try {
        const client = new MongoClient(url, { useUnifiedTopology: true });
        await client.connect();

        const database = client.db('quote');
        const collection = database.collection('quotes');

        const quote = req.body.quote;
        const author = req.body.author;

        await collection.insertOne({ quote, author });

        return "Quote added successfully";
    } catch (error) {
        console.error(error);
        return "Error adding quote";
    } finally {
        await client.close();
    }
})

And here is the front end (public/index.html):


<form hx-post="/add-quote">
    <input type="text" name="quote" placeholder="Enter quote">
    <input type="text" name="author" placeholder="Enter author">
<button type="submit">Add Quote</button>

When you enter an author and quote, and hit Add Quote itโ€™ll be added to the database.ย If you click Load Data, you will see your update in the list.ย It should look something like this:

HTMX table with two rows and text. IDG

If you look at both the server and client for the application so far, you can see that weโ€™re doing a bare minimum of work.ย The biggest thing HTMX has streamlined here is submitting the form.ย The hx-post attribute replaces all the work of taking the data off the form, marshaling it into JSON, and submitting it with fetch() or something similar.

Conclusion

As things become more complex, you begin having to rely on JavaScript in the client, even with HTMX.ย For example, inline row editing.ย Some things that you might expect to use JavaScript for, like inserting the new rows directly into the table, can be done with HTMX swapping.ย HTMX lets you do a lot with its simple syntax and then fall back to JavaScript when necessary.ย 

The biggest mental change is in generating HTMX from the server.ย You have your choice of several high-end HTML or JavaScript templating engines to make this much easier.ย Once you are used to working with HTMX, itโ€™s a breeze. Essentially, youโ€™ve eliminated the whole layer of JSON conversion from the stack.

Weโ€™ve just taken the quickest run through combining bun, Elysia, HTMX, and MongoDB, but you should at least have a feel for this stack.ย The components work well together without any unnecessary friction.ย Bun, Elysia, and MongoDB quietly do their jobs, while HTMX takes a little more thought if you are more accustomed to JSON APIs. Find the code for this article on myย GitHub repository. Weโ€™ll work more with this example in the next article, where weโ€™ll use Pug to add some fancy interactions into the mix.

Matthew Tyson
Contributing Writer

Matthew Tyson is a contributing writer at InfoWorld. A seasoned technology journalist and expert in enterprise software development, Matthew has written about programming, programming languages, language frameworks, application platforms, development tools, databases, cryptography, information security, cloud computing, and emerging technologies such as blockchain and machine learning for more than 15 years. His work has appeared in leading publications including InfoWorld, CIO, CSO Online, and IBM developerWorks. Matthew also has had the privilege of interviewing many tech luminaries including Brendan Eich, Grady Booch, Guillermo Rauch, and Martin Hellman.

Matthewโ€™s diverse background encompasses full-stack development (Java, JVM languages such as Kotlin, JavaScript, Python, .NET), front-end development (Angular, React, Vue, Svelte) and back-end development (Spring Boot, Node.js, Django), software architecture, and IT infrastructure at companies ranging from startups to Fortune 500 enterprises. He is a trusted authority in critical technology areas such as database design (SQL and NoSQL), AI-assisted coding, agentic AI, open-source initiatives, enterprise integration, and cloud platforms, providing insightful analysis and practical guidance rooted in real-world experience.

More from this author