DEV Community

Rafał Goławski
Rafał Goławski

Posted on

Spin up Bun + Preact full-stack app in minutes 🚀

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 
Enter fullscreen mode Exit fullscreen mode

That’s it - nothing fancy, just enough to get a full-stack Bun app running.

1. Initialize the project

bun init 
Enter fullscreen mode Exit fullscreen mode

This scaffolds your package.json, tsconfig.json, and a few handy defaults.

2. Install Preact

bun add preact 
Enter fullscreen mode Exit fullscreen mode

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" } } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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" } } 
Enter fullscreen mode Exit fullscreen mode
  • 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> 
Enter fullscreen mode Exit fullscreen mode

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}`) 
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen 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)