npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bunsrv

v0.0.5

Published

Middleware-enabled HTMLBundle server for Bun

Readme

bunsrv

bunsrv is a middleware-aware server layer for Bun.

It combines:

  • Bun HTMLBundle serving
  • static directory serving
  • middleware around bundles, APIs, and opted-in static routes
  • memory, disk, and hybrid bundle storage
  • byte ranges and conditional responses for bundle assets
  • dynamic bundle registration and replacement
  • configurable static index behavior
  • optional generated directory listings

By default, static files use Bun's native { dir } resolver. An explicit staticMode:"file" route instead uses confined path resolution followed by Bun.file().

Install

bun add bunsrv

Quick start

import {
  createBundledServer,
} from "bunsrv";

import {
  basicAuth,
  logger,
} from "bunsrv/middleware";

import mainBundle from "./index.html";

const app =
  await createBundledServer({
    port: 3000,

    routes: {
      // HTMLBundle
      "/main":
        mainBundle,

      // Fast path: direct Bun-native static serving
      "/static/*": {
        dir: "./public",
      },

      // Static through bunsrv middleware
      "/private/*": {
        dir: "./private",
        middleware: true,
      },

      // Browseable directory:
      // index.html first, listing when no index exists
      "/files/*": {
        dir: "./files",
        directoryListing: true,
      },
    },

    fetch(req) {
      const url =
        new URL(req.url);

      if (
        url.pathname ===
        "/api/hello"
      ) {
        return Response.json({
          message: "hello",
        });
      }

      return new Response(
        "Not found",
        {
          status: 404,
        },
      );
    },
  });

app.use(logger());

app.use(
  basicAuth({
    username: "admin",
    password: "secret",
    routes: ["/private"],
  }),
);

The routes above use different paths internally:

| Route | Middleware | Static strategy | |---|---:|---| | /main | yes | bunsrv bundle cache | | /static/* | no | direct Bun-native { dir } | | /private/* | yes | bunsrv → HTTP over the resolved private transport → private Bun-native { dir } | | /files/* | no | bunsrv listing/index logic → loopback → private Bun-native { dir } | | /api/hello | yes | config.fetch |

A static route can be bunsrv-managed without running middleware. directoryListing, topLevelIndex:false, index:"bunsrv", index:false, or a custom indexFile require bunsrv-managed routing; middleware only runs when middleware:true.


HTML bundles

Register an imported Bun HTML bundle:

import appBundle from "./index.html";

const app =
  await createBundledServer({
    routes: {
      "/app":
        appBundle,
    },
  });

Or pass an HTML entry path and let bunsrv build it:

const app =
  await createBundledServer({
    routes: {
      "/app":
        "./index.html",
    },
  });

Bundle entrypoints and generated assets always pass through the bunsrv middleware pipeline.

app.use(
  basicAuth({
    username: "admin",
    password: "secret",
    routes: ["/app"],
  }),
);

Protecting /app also protects generated JavaScript, CSS, images, and other bundle outputs below that prefix.

For storage modes, ranges, conditional requests, hot replacement, and dynamic bundle registration, see README-bundles.md.


Static directories

Direct Bun-native static

The simplest static route is:

routes: {
  "/static/*": {
    dir: "./public",
  },
}

This is the fastest path. Bun handles the directory route directly and bunsrv middleware is not involved.

Static serving modes

Each static route can choose an implementation:

type DirectoryStaticMode =
  | "auto"
  | "native"
  | "loopback"
  | "file";

"auto" is the default and preserves the current behavior:

plain static route
  -> bun-native

route needing middleware/listing/bunsrv index behavior
  -> managed loopback

Force a mode per route:

routes: {
  "/native/*": {
    dir: "./public",
    staticMode: "native",
  },

  "/loopback/*": {
    dir: "./public",
    staticMode: "loopback",
  },

  "/file/*": {
    dir: "./public",
    staticMode: "file",
  },
}

Or set a server default:

const app =
  await createBundledServer({
    staticMode: "file",
  });

A route-level staticMode overrides the server default.

staticMode:"file" performs confined path resolution inside the configured root and serves regular files with Bun.file(). It supports middleware, indexes, listings, conditional requests, and Bun's native file range handling without creating a private static server.

Static routes can also override the server range policy:

"/downloads/*": {
  dir: "./downloads",
  staticMode: "loopback",
  ranges: false,
}

Resolution is:

const ranges =
  route.ranges ??
  server.ranges ??
  true;

ranges:false is enforceable by loopback routes. In staticMode:"auto", it promotes a plain route to loopback. Explicit "native" and "file" routes reject ranges:false because Bun owns range handling there.

Static with middleware

Opt in with:

routes: {
  "/private/*": {
    dir: "./private",
    middleware: true,
  },
}

With automatic mode, middleware:true selects the managed loopback implementation:

public request
  ↓
bunsrv middleware
  ↓
loopback HTTP over AF_UNIX/TCP
  ↓
private Bun native { dir }

A route can explicitly use:

staticMode: "file"

to run middleware and then serve the regular file with Bun.file() instead. Bun 1.4's Server.fetch() does not dispatch native { dir } routes, which is why the loopback implementation uses real HTTP.

Index behavior

type DirectoryIndexMode =
  | "bun"
  | "bunsrv"
  | false;

Example:

routes: {
  "/docs/*": {
    dir: "./docs",
    middleware: true,
    index: "bunsrv",
    indexFile: "index.html",
  },
}
  • "bun" leaves nested directory index behavior to Bun.
  • "bunsrv" explicitly selects the configured index file, then delegates the actual file read to Bun.
  • false disables index files for directories.

Top-level index

Control the static mount root independently:

routes: {
  "/files/*": {
    dir: "./files",
    index: "bunsrv",
    topLevelIndex: false,
    directoryListing: true,
  },
}

Now:

/files/
  -> generated directory listing

/files/docs/
  -> docs/index.html when it exists
  -> generated listing when it does not

topLevelIndex defaults to true.

Directory listings

Directory listings are off by default.

routes: {
  "/files/*": {
    dir: "./files",
    directoryListing: true,
  },
}

With the normal index behavior, bunsrv serves the index file first and generates a listing only when the index is missing.

To always list directories:

routes: {
  "/files/*": {
    dir: "./files",
    index: false,
    directoryListing: true,
  },
}

Generated listing HTML can also pass through middleware:

routes: {
  "/private-files/*": {
    dir: "./private-files",
    middleware: true,
    directoryListing: true,
  },
}

See README-static.md for the full static routing, index, listing, security, and caching behavior.


Static route options

type StaticTransport =
  | "auto"
  | "tcp"
  | "unix"
  | {
      type: "unix";
      path?: string;
    };

interface DirectoryRouteConfig {
  dir: string;
  staticMode?: "auto" | "native" | "loopback" | "file";
  ranges?: boolean;
  statCache?: boolean;
  middleware?: boolean;
  index?: "bun" | "bunsrv" | false;
  indexFile?: string;
  topLevelIndex?: boolean;
  directoryListing?: boolean;
}

Server-level managed-static transport:

const app =
  await createBundledServer({
    staticTransport:
      "auto",
  });

"auto" tries AF_UNIX first and falls back to 127.0.0.1 TCP if the current Bun/platform cannot use the socket. Use "tcp" or "unix" to require a specific transport. A custom socket path is supported with { type:"unix", path:"..." }.

Important defaults:

| Option | Default | |---|---| | route staticMode | server staticMode or "auto" | | server staticMode | "auto" | | route ranges | server ranges or true | | server ranges | true | | middleware | false | | direct static index | "bun" | | managed static index | "bunsrv" | | indexFile | "index.html" | | topLevelIndex | true | | directoryListing | false | | server staticTransport | "auto" |


Middleware

Middleware uses around-style async composition:

import type {
  Middleware,
} from "bunsrv";

const timing: Middleware =
  async (req, next) => {
    const started =
      performance.now();

    const response =
      await next();

    console.log(
      req.method,
      new URL(req.url).pathname,
      response.status,
      performance.now() -
        started,
    );

    return response;
  };

app.use(timing);

Built-in middleware includes:

import {
  basicAuth,
  bodyLimit,
  cacheControl,
  cors,
  errorHandler,
  headers,
  logger,
  mutateHtml,
  mutateJson,
  requestId,
  rewrite,
  securityHeaders,
} from "bunsrv/middleware";

Direct Bun-native static routes do not run the bunsrv middleware stack. Static routes run middleware only when configured with middleware:true.

For built-in options, ordering, request replacement, response mutation, and custom middleware, see README-middleware.md.


Bundle storage

Global storage:

const app =
  await createBundledServer({
    storage: {
      type: "hybrid",
      maxMemoryAssetSize:
        512 * 1024,
    },
  });

Supported modes:

type BundleStorage =
  | "memory"
  | "disk"
  | {
      type: "hybrid";
      maxMemoryAssetSize?: number;
    };

Hybrid storage is the default. Small assets stay memory-backed and larger assets remain disk-backed.

Per-bundle storage is also supported:

routes: {
  "/media": {
    source:
      "./media.html",
    storage:
      "disk",
  },
}

Dynamic bundles

Add or replace a bundle:

await app.addBundle(
  "./admin.html",
  "/admin",
);

Choose storage for that registration:

await app.addBundle(
  "./media.html",
  "/media",
  {
    storage: "disk",
  },
);

Remove a bundle:

app.removeBundle(
  "/admin",
);

Bundle replacement is staged before the live route map is changed, and generation tracking prevents an older slow build from replacing a newer generation.


Fallback handler

Use fetch for APIs or other unmatched requests:

const app =
  await createBundledServer({
    fetch(req) {
      if (
        new URL(req.url)
          .pathname ===
        "/api/health"
      ) {
        return Response.json({
          ok: true,
        });
      }

      return new Response(
        "Not found",
        {
          status: 404,
        },
      );
    },
  });

The fallback handler runs through middleware.


Startup route table

Unless quiet:true is set, bunsrv prints the configured route mounts and serving behavior:

bunsrv http://localhost:3000/

Static delegate:
  requested: auto
  transport: unix
  socket:    /tmp/bunsrv-ab12cd34.sock
  routes:    2

ROUTE         TYPE    MIDDLEWARE  STRATEGY       INDEX               TOP-INDEX  LISTING  RANGES  TARGET
------------  ------  ----------  -------------  ------------------  ---------  -------  ------  ------------
/main         bundle  yes         bunsrv         -                   -          -        yes     hybrid
/static/*     static  no          bun-native     bun                 yes        no       bun     ./public
/private/*    static  yes         loopback:unix  bunsrv:index.html   yes        no       yes     ./private
/files/*      static  no          loopback:unix  bunsrv:index.html   yes        yes      yes     ./files
/downloads/*  static  yes         bun-file       bunsrv:index.html   yes        no       bun     ./downloads
<unmatched>   fetch   yes         bunsrv         -                   -          -        -       config.fetch

Suppress it:

const app =
  await createBundledServer({
    quiet: true,
  });

CLI applications can map --quiet / -q to this option.


Shutdown

Use:

await app.close();

This stops the public server, stops the private loopback static server when one exists, and removes bunsrv-owned temporary bundle build output.

To close active connections immediately:

await app.close(true);

Public API

import {
  createBundledServer,
} from "bunsrv";

import type {
  AssetStorage,
  BundleRegistrationOptions,
  BundleRoute,
  BundleRouteConfig,
  BundleServeConfig,
  BundleSource,
  BundleStorage,
  BundledServer,
  CachedAsset,
  DirectoryIndexMode,
  DirectoryRouteConfig,
  DirectoryStaticMode,
  FallbackHandler,
  HybridStorageOptions,
  Middleware,
  Next,
  StaticTransport,
  StaticUnixTransportOptions,
  StorageStats,
} from "bunsrv";

Main server methods:

app.use(middleware);

await app.addBundle(
  source,
  prefix,
  options,
);

app.removeBundle(prefix);

app.storageStats();

await app.close();

Detailed documentation