> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/oven-sh/bun/llms.txt
> Use this file to discover all available pages before exploring further.

# Fullstack Development

> Build fullstack applications with Bun's integrated dev server that bundles frontend assets and serves API routes in a single process.

Bun's `Bun.serve()` integrates directly with the bundler, letting you serve HTML, TypeScript, JSX, and CSS from a single server process with hot module reloading in development and optimized production builds.

## Quick start

Import HTML files and pass them to the `routes` option in `Bun.serve()`:

```typescript server.ts theme={null}
import { serve } from "bun";
import homepage from "./index.html";
import dashboard from "./dashboard.html";

const server = serve({
  routes: {
    "/": homepage,
    "/dashboard": dashboard,

    "/api/users": {
      async GET(req) {
        return Response.json(await getUsers());
      },
      async POST(req) {
        const body = await req.json();
        return Response.json(await createUser(body), { status: 201 });
      },
    },
  },

  development: true,
});

console.log(`Listening on ${server.url}`);
```

```bash theme={null}
bun run server.ts
```

## HTML routes

### HTML as an entrypoint

The web starts with HTML, and so does Bun's fullstack dev server. Import an HTML file directly from your TypeScript or JavaScript server code:

```typescript theme={null}
import homepage from "./index.html";
import dashboard from "./dashboard.html";
```

Pass these to `routes` in `Bun.serve()`:

```typescript theme={null}
Bun.serve({
  routes: {
    "/": homepage,
    "/dashboard": dashboard,
  },
});
```

When a request arrives for `/`, Bun scans the HTML for `<script>` and `<link>` tags, runs the bundler on the referenced files, and serves the result.

### What Bun does to your HTML

An `index.html` like this:

```html index.html theme={null}
<!DOCTYPE html>
<html>
  <head>
    <title>Home</title>
    <link rel="stylesheet" href="./reset.css" />
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./sentry-init.ts"></script>
    <script type="module" src="./app.tsx"></script>
  </body>
</html>
```

Gets transformed into:

```html theme={null}
<!DOCTYPE html>
<html>
  <head>
    <title>Home</title>
    <link rel="stylesheet" href="/index-[hash].css" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/index-[hash].js"></script>
  </body>
</html>
```

Multiple `<script>` tags are combined into a single bundle, and multiple CSS files are merged into one stylesheet. Asset URLs are content-hashed for cache busting.

### Processing pipeline

<Steps>
  <Step title="Script processing">
    Transpiles TypeScript, JSX, and TSX from `<script>` tags. Bundles imported dependencies. Generates sourcemaps for debugging. Minifies when `development` is `false`.
  </Step>

  <Step title="CSS processing">
    Processes `<link rel="stylesheet">` tags. Concatenates CSS files and rewrites `url()` references to include content-addressable hashes.
  </Step>

  <Step title="Asset processing">
    Rewrites image and font URLs to include content-addressable hashes. Small assets in CSS are inlined as `data:` URLs to reduce HTTP requests.
  </Step>

  <Step title="HTML rewriting">
    Combines all `<script>` tags into one and all `<link>` tags into one, producing a new HTML file that references the bundled assets.
  </Step>

  <Step title="Serving">
    Bundled files are exposed as static routes using Bun's built-in static file serving. The same mechanism as passing a `Response` to `static` in `Bun.serve()`.
  </Step>
</Steps>

## React integration

<CodeGroup>
  ```typescript server.ts theme={null}
  import { serve } from "bun";
  import homepage from "./public/index.html";

  serve({
    routes: {
      "/": homepage,
    },
    async fetch(req) {
      return new Response("Not found", { status: 404 });
    },
  });
  ```

  ```html public/index.html theme={null}
  <!DOCTYPE html>
  <html>
    <head>
      <title>My App</title>
      <link rel="stylesheet" href="../src/styles.css" />
    </head>
    <body>
      <div id="root"></div>
      <script type="module" src="../src/main.tsx"></script>
    </body>
  </html>
  ```

  ```tsx src/main.tsx theme={null}
  import { createRoot } from "react-dom/client";
  import { App } from "./App";

  const root = createRoot(document.getElementById("root")!);
  root.render(<App />);
  ```

  ```tsx src/App.tsx theme={null}
  import { useState } from "react";

  export function App() {
    const [count, setCount] = useState(0);
    return (
      <div>
        <h1>Hello from Bun</h1>
        <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      </div>
    );
  }
  ```
</CodeGroup>

No Webpack, Vite, or Create React App required. Bun handles transpilation and bundling automatically.

## Development mode

Enable development mode with `development: true`:

```typescript theme={null}
Bun.serve({
  routes: { "/": homepage },
  development: true,
});
```

In development mode, Bun:

* Includes sourcemaps so devtools show original source
* Disables minification
* Re-bundles assets on each request to an `.html` route
* Enables hot module reloading (HMR)
* Echoes `console.log` calls from the browser to the terminal

### Hot module replacement

HMR is enabled by default in development mode. To configure it explicitly:

```typescript theme={null}
Bun.serve({
  routes: { "/": homepage },
  development: {
    hmr: true,
    console: true, // Forward browser console to terminal
  },
});
```

When `console: true` is set, `console.log()`, `console.warn()`, and `console.error()` calls from your frontend code are forwarded to the terminal over the same WebSocket connection used for HMR.

### Development vs production comparison

| Feature            | Development     | Production |
| ------------------ | --------------- | ---------- |
| Source maps        | Enabled         | Disabled   |
| Minification       | Disabled        | Enabled    |
| Hot reloading      | Enabled         | Disabled   |
| Asset bundling     | On each request | Cached     |
| Console forwarding | Supported       | Disabled   |
| Error details      | Full            | Minimal    |

## API routes

### HTTP method handlers

```typescript theme={null}
Bun.serve({
  routes: {
    "/api/users": {
      async GET(req) {
        const users = await db.query("SELECT * FROM users").all();
        return Response.json(users);
      },
      async POST(req) {
        const { name, email } = await req.json();
        const user = await db.query(
          "INSERT INTO users (name, email) VALUES (?, ?) RETURNING *"
        ).get(name, email);
        return Response.json(user, { status: 201 });
      },
      async DELETE(req) {
        await db.query("DELETE FROM users WHERE id = ?").run(req.params.id);
        return new Response(null, { status: 204 });
      },
    },
  },
});
```

### Dynamic routes

```typescript theme={null}
Bun.serve({
  routes: {
    // Single URL parameter
    "/api/users/:id": async (req) => {
      const { id } = req.params;
      const user = await getUserById(id);
      return Response.json(user);
    },

    // Multiple parameters
    "/api/users/:userId/posts/:postId": async (req) => {
      const { userId, postId } = req.params;
      return Response.json(await getPost(userId, postId));
    },

    // Wildcard
    "/api/files/*": async (req) => {
      const path = req.params["*"];
      return new Response(await readFile(path));
    },
  },
});
```

## Production builds

### Ahead-of-time bundling (recommended)

Use `bun build` to bundle your full-stack application before deployment:

```bash theme={null}
bun build --target=bun --production --outdir=dist ./server.ts
```

When the bundler sees an HTML import in server-side code, it bundles the frontend assets and replaces the import with a manifest object that `Bun.serve()` uses to serve pre-bundled assets.

```bash theme={null}
NODE_ENV=production bun dist/index.js
```

### Runtime bundling

Set `development: false` to enable in-memory caching without a build step:

```typescript theme={null}
Bun.serve({
  routes: { "/": homepage },
  development: false, // Bundle on first request, then cache
});
```

With `development: false`, Bun:

* Bundles assets lazily on the first request
* Caches the result in memory until the server restarts
* Adds `Cache-Control` and `ETag` headers
* Minifies JavaScript

### Docker deployment

```dockerfile Dockerfile theme={null}
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --target=bun --production --outdir=dist ./server/index.ts

FROM oven/bun:1-slim
WORKDIR /app
COPY --from=builder /app/dist ./
EXPOSE 3000
CMD ["bun", "index.js"]
```

## Plugins

Bundler plugins work when bundling static routes. Configure them in `bunfig.toml`:

```toml bunfig.toml theme={null}
[serve.static]
plugins = ["bun-plugin-tailwind"]
```

### TailwindCSS

```bash theme={null}
bun add tailwindcss bun-plugin-tailwind
```

```toml bunfig.toml theme={null}
[serve.static]
plugins = ["bun-plugin-tailwind"]
```

```html index.html theme={null}
<link rel="stylesheet" href="tailwindcss" />
```

### Custom plugins

```toml bunfig.toml theme={null}
[serve.static]
plugins = ["./my-plugin.ts"]
```

```typescript my-plugin.ts theme={null}
import type { BunPlugin } from "bun";

const myPlugin: BunPlugin = {
  name: "my-custom-plugin",
  setup(build) {
    build.onLoad({ filter: /\.custom$/ }, async (args) => {
      const text = await Bun.file(args.path).text();
      return {
        contents: `export default ${JSON.stringify(text)};`,
        loader: "js",
      };
    });
  },
};

export default myPlugin;
```

## Inline environment variables

Configure how `process.env.*` references are handled in frontend code:

```toml bunfig.toml theme={null}
[serve.static]
env = "PUBLIC_*"   # Only inline env vars with this prefix (recommended)
# env = "inline"   # Inline all env vars
# env = "disable"  # Disable env var inlining (default)
```

<Warning>
  Only literal `process.env.FOO` references are replaced — not `import.meta.env` or dynamic access. If an environment variable is not set, you may see `ReferenceError: process is not defined` in the browser.
</Warning>

## Project structure

A recommended structure for a Bun fullstack application:

```
my-app/
├── server/
│   ├── routes/
│   │   ├── users.ts
│   │   └── auth.ts
│   └── index.ts
├── src/
│   ├── components/
│   │   └── App.tsx
│   ├── styles/
│   │   └── globals.css
│   └── main.tsx
├── public/
│   ├── index.html
│   └── dashboard.html
├── bunfig.toml
└── package.json
```

<Note>
  The fullstack dev server is still evolving. CLI integration with `bun build`, file-based API routing, and built-in SSR are planned for future releases.
</Note>
