Skip to content

static-middleware ​

The staticFiles middleware serves static files from a directory on disk. It supports ETag-based caching, range requests, and configurable cache control headers. Requests that match a static file are handled immediately without running downstream middleware or route handlers.

Installation ​

The static middleware is included with Remix. No additional installation is needed.

Import ​

ts
import { staticFiles } from 'remix/static-middleware'

API ​

staticFiles(directory, options?) ​

Returns a middleware function that serves static files from the given directory.

ts
let router = createRouter({
  middleware: [
    staticFiles('./public'),
  ],
})

Parameters:

ParameterTypeDescription
directorystringPath to the directory containing static files. Resolved relative to the current working directory.
optionsobjectOptional configuration. See Options below.

Options ​

OptionTypeDefaultDescription
cacheControlstring | ((path: string) => string)'public, max-age=0'The Cache-Control header value. Can be a string or a function that receives the file path and returns a string.
etagbooleantrueWhether to generate and check ETag headers for conditional requests (If-None-Match).
lastModifiedbooleantrueWhether to set the Last-Modified header and handle If-Modified-Since conditional requests.
extensionsstring[]undefinedFile extensions to try when the URL does not have one. For example, ['html'] makes /about try /about.html.
indexstring | false'index.html'The filename to serve for directory requests. Set to false to disable directory index files.

Behavior ​

When a request arrives, the middleware:

  1. Maps the URL path to a file in the configured directory.
  2. If no file matches (and no extensions or index fallback matches), calls next() to pass the request to downstream middleware.
  3. If a file matches:
    • Generates an ETag from the file content hash (if etag is enabled).
    • Checks If-None-Match and If-Modified-Since headers. Returns 304 Not Modified if the file has not changed.
    • Handles Range requests for partial content (206 Partial Content).
    • Sets Content-Type based on the file extension.
    • Sets Cache-Control, ETag, Last-Modified, and Accept-Ranges headers.
    • Returns the file content as the response body.

Because matching requests are handled immediately, downstream middleware (like CSRF, session, etc.) is not run for static file requests. This improves performance and avoids unnecessary overhead.

ETag Generation ​

ETags are generated from a hash of the file contents. This means:

  • The same file always produces the same ETag, even across server restarts.
  • Modified files produce a different ETag immediately.
  • Browsers that send If-None-Match receive a 304 response with no body, saving bandwidth.

Range Request Support ​

The middleware supports the Range header for partial content delivery. This is important for:

  • Video and audio streaming --- Browsers request byte ranges to enable seeking.
  • Large file downloads --- Clients can resume interrupted downloads.

Range responses include the Content-Range header and return 206 Partial Content.

Examples ​

Basic Usage ​

Serve files from a public directory:

ts
import { createRouter } from 'remix/fetch-router'
import { staticFiles } from 'remix/static-middleware'

let router = createRouter({
  middleware: [
    staticFiles('./public'),
  ],
})

A file at ./public/css/style.css is served at the URL /css/style.css.

Long-Lived Cache for Hashed Assets ​

Use a function to set different cache policies based on file path:

ts
staticFiles('./public', {
  cacheControl(path) {
    // Hashed assets can be cached forever
    if (path.includes('/assets/')) {
      return 'public, max-age=31536000, immutable'
    }
    // Everything else gets a short cache
    return 'public, max-age=60'
  },
})

Extension Fallbacks ​

Serve .html files without the extension in the URL:

ts
staticFiles('./public', {
  extensions: ['html'],
})

A request for /about will try ./public/about, then ./public/about.html.

Disable Directory Index ​

ts
staticFiles('./public', {
  index: false,
})

Requests for / will not serve ./public/index.html and will instead pass through to route handlers.

With Compression ​

Place the compression middleware before staticFiles so that static file responses are compressed:

ts
import { compression } from 'remix/compression-middleware'
import { staticFiles } from 'remix/static-middleware'

let router = createRouter({
  middleware: [
    compression(),
    staticFiles('./public'),
  ],
})

Released under the MIT License.