Bun

Building a single-page application with Bun's bundler


Colin McDonnell · April 24, 2023

With Bun's new bundler, it's now possible to build a fullstack React app with server-side rendering entirely within the Bun ecosystem! Let's work an example and see how this looks.

Let's initialize an empty project and install some dependencies.

mkdir my-bun-app
cd my-bun-app
bun init -y
mv index.ts index.tsx
bun add react@next react-dom@next

We now have an index.tsx file in our project root. Let's write a simple React component and render it to the DOM.

import { createRoot } from "react-dom/client";

export default function App() {
  return (
    <html>
      <body>
        <h1>Hello world</h1>
      </body>
    </html>
  );
}

const root = createRoot(document);
root.render(<App />);

This doesn't do anything yet. We need to spin up a simple HTTP server. Next, create a file called server.ts and paste the following code. This spins up a simple HTTP server that returns an empty HTML document on every request.

server.ts
Bun.serve({
  port: 3000,
  async fetch(req) {
    return new Response(
      `<html>Hello</html>`,
      {headers: {'content-type': 'text/html'}}
    );
  },
});

Let's run the server and see what happens.

bun run server.ts
open http://localhost:3000

You should see a message saying "Hello". This is the simple HTML document we returned from our server.

It's bundling time! Let's wire up our server and our React component. First add a call to Bun.build above the server. This bundles index.tsx and writes the result to ./build/index.js.

server.ts
const BUILD_DIR = import.meta.dir + '/build';
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: BUILD_DIR,
  target: 'browser',
});

Bun.serve({
  // ...
})

Finally, let's tweak our server to serve ./build/index.js and accordingly add a <script> tag accordingly.

server.ts
const BUILD_DIR = import.meta.dir + '/build';
await Bun.build({
  entrypoints: ['./index.tsx'],
  outdir: BUILD_DIR,
  target: 'browser',
});

Bun.serve({
  port: 4000,
  async fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === '/index.js') {
      const bundled = Bun.file(BUILD_DIR + '/index.js');
      return new Response(bundled);
    }

    return new Response(
      `<html>
        <script src="/index.js"></script>
      </html>`,
      {headers: {'content-type': 'text/html'}}
    );
  },
});

Refresh the page, and you should see "Hello world", the rendered version of your <App /> component.