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.