Last Updated: March 2026
Building a modern web application no longer requires a towering pile of dependencies and configuration files. By combining Bun, Astro, TypeScript, and HTMX — playfully dubbed the BATH stack — you get a development experience that is fast to set up, light on client-side JavaScript, and pleasant to work with day to day. This guide walks you through the ideas behind each tool, then shows you how to wire them together into a working project.
What is the BATH stack?
The name is a cheeky acronym: Bun, Astro, TypeScript, HTMX. Each piece solves a different part of the web-development puzzle, and they complement one another well.
Bun is an all-in-one JavaScript and TypeScript toolkit. It bundles a runtime, a package manager, a test runner, and a bundler into a single binary. Written in Zig and powered by Apple's JavaScriptCore engine, Bun is designed for speed — package installs, script execution, and server startup all happen noticeably faster than with traditional Node.js tooling. It handles TypeScript and JSX natively, so there is no separate transpilation step to configure. Bun reached version 1.0 in September 2023 and has been under rapid development since; its recent 1.3 release line introduced improvements to garbage collection, idle CPU usage, and Node.js compatibility. In late 2025, Anthropic acquired Oven (the company behind Bun), though Bun remains open source and MIT-licensed.
Astro is a frontend framework built for content-driven websites — blogs, marketing pages, documentation sites, and e-commerce storefronts. Its key insight is that most web pages are mostly static. Astro renders pages to plain HTML on the server and ships zero JavaScript to the browser by default. When you do need interactivity, Astro's "islands" architecture lets you hydrate only the specific components that require it, leaving the rest of the page as lightweight HTML. Astro supports every major UI library (React, Vue, Svelte, Solid, and more), but it works perfectly well with none of them at all. The Astro team recently joined Cloudflare, and version 6 is currently in beta with a redesigned development server and first-class Cloudflare Workers support.
TypeScript needs little introduction. It adds static types to JavaScript, catching a broad class of bugs at compile time rather than at runtime. Bun runs TypeScript files directly, and Astro has deep TypeScript integration — including typed frontmatter validation for Markdown content. In the BATH stack, TypeScript is the common language everything is written in.
HTMX is a small library (roughly 14 KB minified and gzipped) that gives HTML itself superpowers. Instead of writing client-side JavaScript to make AJAX requests and update the DOM, you add attributes like hx-get, hx-post, and hx-swap directly to your HTML elements. The server responds with a chunk of HTML, and HTMX swaps it into the page. This keeps interactivity on the server side where your data and logic already live, and it means you can build dynamic, responsive interfaces without shipping a JavaScript framework to the browser.
Together, these four tools form a stack that is server-first, fast by default, and refreshingly simple.
Why this combination works
The BATH stack is not trying to compete with single-page application frameworks like Next.js or SvelteKit for every use case. It shines when your goal is a content-heavy or form-driven site where most of the rendering belongs on the server.
Astro already eliminates unnecessary client-side JavaScript. HTMX takes that philosophy further by letting you add dynamic behavior — loading new content, submitting forms, updating sections of a page — through HTML attributes rather than JavaScript code. Bun makes the developer experience fast: installs are near-instant, the dev server starts quickly, and TypeScript works without extra tooling. The result is a workflow where you spend most of your time writing HTML, CSS, and small server-side endpoints, rather than wrestling with build configuration.
Setting up your project
Prerequisites
You need Bun installed on your machine. If you do not already have it, install it from the terminal:
curl -fsSL https://bun.sh/install | bash
Verify the installation:
bun --version
Scaffolding an Astro project with Bun
Astro's create command supports Bun out of the box. Run the following to scaffold a new project:
bun create astro@latest my-bath-app
The interactive wizard will ask a few questions. Choose "Include sample files" to get a working starting point, select "Yes" for installing dependencies, pick "Strict" or "Strictest" for TypeScript, and initialize a git repository if you like.
Once scaffolding is complete, move into the project directory:
cd my-bath-app
Adding HTMX
There are two ways to bring HTMX into an Astro project. The simplest is to use the community-maintained astro-htmx integration, which automatically includes the HTMX script on every page.
Install it alongside the HTMX library:
bun add astro-htmx htmx.org
Then register the integration in your Astro configuration file:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import htmx from 'astro-htmx';
export default defineConfig({
integrations: [htmx()],
});
If you prefer more control over where and how HTMX loads, you can skip the integration and add the script tag manually to a layout file:
<!-- src/layouts/Layout.astro -->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My BATH App</title>
<script src="https://unpkg.com/htmx.org@2"></script>
</head>
<body>
<slot />
</body>
</html>
Either approach works. The integration is more convenient; the manual approach gives you fine-grained control.
Building a dynamic page with HTMX and Astro partials
Here is where the BATH stack starts to feel different from a typical JavaScript application. Instead of writing a React component that fetches JSON from an API and renders a list, you write a bit of HTML with an HTMX attribute, and Astro returns an HTML fragment from the server.
The main page
Open src/pages/index.astro and replace its contents:
---
import Layout from '../layouts/Layout.astro';
---
<Layout>
<h1>Product Catalog</h1>
<p>Our current offerings:</p>
<ul hx-get="/partials/products/list" hx-trigger="load" hx-swap="innerHTML">
<li>Loading...</li>
</ul>
</Layout>
The hx-get attribute tells HTMX to make a GET request to /partials/products/list when the page loads (hx-trigger="load"). The response will replace the contents of the <ul> element (hx-swap="innerHTML").
The partial
Astro partials are pages that return a fragment of HTML rather than a full document. Create the directory structure and file:
mkdir -p src/pages/partials/products
touch src/pages/partials/products/list.astro
Open src/pages/partials/products/list.astro and add:
---
export const partial = true;
interface Product {
id: number;
name: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: "Rubber Duck", price: 4.99 },
{ id: 2, name: "Bubble Bath", price: 8.50 },
{ id: 3, name: "Bath Bomb", price: 6.00 },
{ id: 4, name: "Loofah", price: 3.25 },
];
---
{products.map((product) => (
<li>
<strong>{product.name}</strong> — ${product.price.toFixed(2)}
</li>
))}
The export const partial = true line is important. It tells Astro that this page should return only the HTML in the template section, without wrapping it in a full document (no <!DOCTYPE>, no <html> tag). This is exactly what HTMX expects: a fragment of markup it can swap into an existing page.
Notice that the data-fetching logic lives in the frontmatter section (the fenced block between the --- markers). In a real application, you would replace the hardcoded array with a database query, an API call, or a read from a CMS. Because this runs on the server, your database credentials and business logic never reach the browser.
Running the development server
Start the project:
bun run dev
Open your browser to the address Astro prints (typically http://localhost:4321). You should see the heading, the brief "Loading..." text flash, and then the product list appear as HTMX fetches and inserts the partial.
Adding interactivity: a search example
To see how HTMX handles user input, let's add a simple search feature. Update src/pages/index.astro:
---
import Layout from '../layouts/Layout.astro';
---
<Layout>
<h1>Product Catalog</h1>
<input
type="search"
name="q"
placeholder="Search products..."
hx-get="/partials/products/search"
hx-trigger="input changed delay:300ms"
hx-target="#results"
hx-swap="innerHTML"
/>
<ul id="results" hx-get="/partials/products/list" hx-trigger="load" hx-swap="innerHTML">
<li>Loading...</li>
</ul>
</Layout>
The search input uses hx-trigger="input changed delay:300ms" to debounce requests — HTMX will wait 300 milliseconds after the user stops typing before sending the query. The hx-target="#results" attribute tells HTMX to put the response into the <ul> below, not into the input itself.
Now create the search partial at src/pages/partials/products/search.astro:
---
export const partial = true;
interface Product {
id: number;
name: string;
price: number;
}
const allProducts: Product[] = [
{ id: 1, name: "Rubber Duck", price: 4.99 },
{ id: 2, name: "Bubble Bath", price: 8.50 },
{ id: 3, name: "Bath Bomb", price: 6.00 },
{ id: 4, name: "Loofah", price: 3.25 },
];
const query = Astro.url.searchParams.get("q")?.toLowerCase() ?? "";
const filtered = allProducts.filter((p) =>
p.name.toLowerCase().includes(query)
);
---
{filtered.length > 0 ? (
filtered.map((product) => (
<li>
<strong>{product.name}</strong> — ${product.price.toFixed(2)}
</li>
))
) : (
<li>No products found.</li>
)}
Type "duck" into the search box. After a brief pause, the list updates to show only "Rubber Duck." Clear the input, and all four products return. There is no client-side JavaScript involved beyond what HTMX provides. The filtering logic runs entirely on the server.
How the pieces fit together
It helps to step back and see the flow of a typical request in the BATH stack:
The browser loads a page. Astro renders it to HTML on the server and sends it to the client. The page includes the HTMX library (about 14 KB) and no other JavaScript.
The user interacts with the page — clicking a button, typing in a search field, scrolling to a lazy-loaded section. HTMX reads the
hx-*attributes on the relevant element and sends an HTTP request to the server.Astro receives the request and routes it to a partial page. The partial's frontmatter runs your TypeScript logic (database queries, API calls, validation), and the template section returns an HTML fragment.
HTMX receives the HTML fragment and swaps it into the designated spot on the page.
There is no JSON serialization step, no client-side state management library, and no virtual DOM diffing. The server produces HTML; the browser displays it. This is the original architecture of the web, enhanced with just enough tooling to make it feel modern.
Project structure
A typical BATH stack project looks like this:
my-bath-app/
astro.config.mjs
package.json
tsconfig.json
src/
layouts/
Layout.astro
pages/
index.astro
partials/
products/
list.astro
search.astro
components/
Header.astro
Footer.astro
public/
favicon.svg
styles/
global.css
The src/pages/ directory is file-based routing: each .astro file becomes a route. The partials/ subdirectory is a convention, not a requirement — you can put your HTMX endpoints wherever you like. Components in src/components/ are reusable pieces of markup you can include in pages or other components. Static assets in public/ are served as-is.
Going further
This quickstart covers the essentials, but there is plenty more you can do with the BATH stack.
Server-side rendering for production. By default, Astro pre-renders pages at build time (static site generation). If you need your partials to hit a live database on every request, enable SSR by adding an adapter. Astro has official adapters for Node, Cloudflare, Vercel, Netlify, and others. With Bun, the Node adapter typically works well, and there is also a community-maintained Bun-specific adapter.
HTMX extensions. HTMX supports extensions for features like WebSocket connections, server-sent events, morphing (for smoother DOM updates), and client-side validation. These are small add-ons that you include alongside the main library.
Forms and mutations. HTMX handles POST, PUT, PATCH, and DELETE requests just as easily as GET. You can build full CRUD interfaces by combining hx-post, hx-delete, and other attributes with server-side partials that perform the mutation and return updated HTML.
CSS transitions. HTMX can apply CSS classes during swaps, making it straightforward to animate content entering and leaving the page. Combine this with Astro's built-in support for the View Transitions API for smooth page-level transitions.
TypeScript everywhere. Because Bun runs TypeScript natively and Astro supports it deeply, you can define shared types for your data models and use them in both your partials and any standalone API endpoints.
When to use the BATH stack
The BATH stack is a strong fit for content-heavy websites that need pockets of interactivity: blogs with search and filtering, product catalogs, documentation sites with live examples, admin dashboards that display server-rendered data, and form-driven applications. It is especially appealing if you value simplicity and want to avoid the complexity of a full client-side framework.
It is less suited to applications that require heavy client-side interactivity — real-time collaborative editors, complex drag-and-drop interfaces, or offline-capable progressive web apps. For those, a client-side framework like React or Svelte remains a better choice, and Astro's islands architecture means you can still use one within a BATH project for the specific components that need it.
The broader trend in web development has been moving back toward server-rendered HTML, and the BATH stack sits squarely in that current. Bun makes the tooling fast. Astro makes the output lean. TypeScript makes the code safe. HTMX makes the pages dynamic. Together, they offer a way to build for the web that feels both modern and refreshingly simple.
Top comments (0)