Skip to main content
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():
server.ts
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}`);
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:
import homepage from "./index.html";
import dashboard from "./dashboard.html";
Pass these to routes in Bun.serve():
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:
index.html
<!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:
<!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

1

Script processing

Transpiles TypeScript, JSX, and TSX from <script> tags. Bundles imported dependencies. Generates sourcemaps for debugging. Minifies when development is false.
2

CSS processing

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

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.
4

HTML rewriting

Combines all <script> tags into one and all <link> tags into one, producing a new HTML file that references the bundled assets.
5

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().

React integration

import { serve } from "bun";
import homepage from "./public/index.html";

serve({
  routes: {
    "/": homepage,
  },
  async fetch(req) {
    return new Response("Not found", { status: 404 });
  },
});
No Webpack, Vite, or Create React App required. Bun handles transpilation and bundling automatically.

Development mode

Enable development mode with development: true:
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:
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

FeatureDevelopmentProduction
Source mapsEnabledDisabled
MinificationDisabledEnabled
Hot reloadingEnabledDisabled
Asset bundlingOn each requestCached
Console forwardingSupportedDisabled
Error detailsFullMinimal

API routes

HTTP method handlers

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

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

Use bun build to bundle your full-stack application before deployment:
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.
NODE_ENV=production bun dist/index.js

Runtime bundling

Set development: false to enable in-memory caching without a build step:
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
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:
bunfig.toml
[serve.static]
plugins = ["bun-plugin-tailwind"]

TailwindCSS

bun add tailwindcss bun-plugin-tailwind
bunfig.toml
[serve.static]
plugins = ["bun-plugin-tailwind"]
index.html
<link rel="stylesheet" href="tailwindcss" />

Custom plugins

bunfig.toml
[serve.static]
plugins = ["./my-plugin.ts"]
my-plugin.ts
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:
bunfig.toml
[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)
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.

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
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.

Build docs developers (and LLMs) love