Static Files

Use serveStatic() from @hornjs/fest/static to serve files through the middleware pipeline.

Import

import { serveStatic } from "@hornjs/fest/static";

Basic Usage

import { serve } from "@hornjs/fest";
import { NodeRuntimeAdapter } from "@hornjs/fest/node";
import { serveStatic } from "@hornjs/fest/static";

serve({
  adapter: new NodeRuntimeAdapter(),
  middleware: [serveStatic({ dir: "./public" })],
  fetch: () => new Response("Not Found", { status: 404 }),
});

serveStatic() is ordinary middleware, so it can be placed anywhere in the middleware chain.

ServeStaticOptions

dir

Base directory used for static file resolution.

methods

Allowed HTTP methods for static serving.

Defaults to:

  • GET
  • HEAD

Requests using other methods fall through to downstream middleware or the final handler.

renderHTML

Optional hook for rewriting HTML content before sending the response.

serveStatic({
  dir: "./public",
  renderHTML: async ({ html, filename, request }) => {
    return new Response(html.replace("</body>", `<p>${filename}</p></body>`), {
      headers: { "content-type": "text/html; charset=utf-8" },
    });
  },
});

Resolution Rules

serveStatic() follows a few built-in path conventions:

  • / -> index.html
  • /about -> tries about.html, then about/index.html
  • explicit extensions are used as-is

This makes it suitable for simple sites, SPA-style HTML fallbacks, and directory-based static content layouts.

Compression Behavior

If the request advertises compression support:

  • Brotli is preferred when the runtime exposes Brotli compression
  • gzip is used as a fallback
  • otherwise the original file stream is returned

The response sets Content-Encoding and Vary: Accept-Encoding when compression is applied.

Runtime Requirements

Static serving depends on runtime capabilities exposed through request.runtime, including:

  • path resolution
  • opening files
  • gzip and optional Brotli transforms

That means support depends on the active runtime adapter. For example, the stream runtime only exposes minimal capabilities, so static serving there is useful only if the host provides equivalent file access primitives.

Fallthrough Behavior

If no matching file is found, serveStatic() simply calls next(request).

That makes it easy to combine static assets with dynamic routes:

serve({
  middleware: [serveStatic({ dir: "./public" })],
  routes: {
    "/api/pathname": (request) => Response.json({ pathname: new URL(request.url).pathname }),
  },
  fetch: () => new Response("Not Found", { status: 404 }),
});