Bun makes full-stack TypeScript feel almost too easy - a fast runtime, a dev server with HMR, and a bundler. In this post we’ll spin up a minimal app that serves an HTML page, mounts a Preact component, and exposes a tiny API.
What we’re building
You’ll end up with:
- A simple Bun server (serves HTML + a tiny API route).
- A Preact client mounted into
#root
. - Scripts for dev (with hot reload) and production.
src/ ├─ index.ts # server entry ├─ index.html # HTML shell └─ client.tsx # client entry
That’s it - nothing fancy, just enough to get a full-stack Bun app running.
1. Initialize the project
bun init
This scaffolds your package.json
, tsconfig.json
, and a few handy defaults.
2. Install Preact
bun add preact
Preact gives us a lightweight React-compatible API. We’ll use it purely on the client for now.
3. Tell TypeScript where JSX comes from
Add this to the compilerOptions
section in your tsconfig.json
file:
{ "compilerOptions": { "jsxImportSource": "preact" } }
Why? With modern JSX runtimes, TypeScript needs to know which library provides the JSX functions. Setting jsxImportSource
to preact
ensures your .tsx
files resolve JSX to Preact’s runtime, avoiding “JSX not found” or mismatched types.
4. Create the app files
mkdir src \ && mv index.ts src/index.ts \ && touch src/index.html \ && touch src/client.tsx
We’ll keep everything in src/
for clarity. index.ts
will be our server entry, index.html
the shell and client.tsx
the Preact entry.
5. Add scripts to package.json
Insert the following into package.json
:
{ "scripts": { "dev": "bun --hot src/index.ts", "start": "NODE_ENV=production bun src/index.ts" } }
-
dev
- starts Bun with hot reload, so whenever any module or file changes, Bun re-runs the file. -
start
- runs in production mode (no hot reload).
6. Create the HTML shell
Insert the following into src/index.html
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="root"></div> <script type="module" src="./client.tsx"></script> </body> </html>
Why? This is the single page our server will return for all routes. The <script type="module">
line points to our client entry, so Bun can bundle/transform it as needed.
7. Write the server
Insert the following into src/index.ts
:
import { serve } from "bun" import index from "./index.html" const server = serve({ routes: { "/*": index, "/api/hello": () => Response.json({ message: "Hello, world!" }), }, // Enable dev features only when NODE_ENV=development development: process.env.NODE_ENV === "development" && { hmr: true, // push client updates without full page reload console: true, // surface browser console logs/errors in terminal }, }) console.log(`Server is running on ${server.url}`)
What happens:
- The server returns
index.html
for any path (/*
) and a JSON response at/api/hello
. - When
NODE_ENV=development
, Bun enables development mode for this server:- HMR streams client updates to the browser.
- Console overlay mirrors browser logs/errors to your terminal.
- In production (
NODE_ENV=production
), those dev features are off - the server just serves routes normally.
8. Write the client
Insert the following into src/client.tsx
:
import { render } from "preact" const root = document.getElementById("root") if (!root) { throw new Error("Root element not found") } function App() { return <h1>Hello, world!</h1> } render(<App />, root)
Why Preact? It uses the same component model as React, so if you already know React you can be productive right away. At the same time it’s much smaller in size, which makes it a good fit for a Bun-powered setup where you want fast cold starts and lightweight bundles.
Run it!
bun run dev # development mode bun run start # production mode
Open the printed URL (typically http://localhost:3000
). Try editing src/client.tsx
or src/index.ts
- you should see instant updates.
That’s it! You now have a tiny full-stack Bun app with Preact, hot reload, and an example API - perfect as a seed for side projects and demos.
Top comments (0)