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
import { staticFiles } from 'remix/static-middleware'API
staticFiles(directory, options?)
Returns a middleware function that serves static files from the given directory.
let router = createRouter({
middleware: [
staticFiles('./public'),
],
})Parameters:
| Parameter | Type | Description |
|---|---|---|
directory | string | Path to the directory containing static files. Resolved relative to the current working directory. |
options | object | Optional configuration. See Options below. |
Options
| Option | Type | Default | Description |
|---|---|---|---|
cacheControl | string | ((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. |
etag | boolean | true | Whether to generate and check ETag headers for conditional requests (If-None-Match). |
lastModified | boolean | true | Whether to set the Last-Modified header and handle If-Modified-Since conditional requests. |
extensions | string[] | undefined | File extensions to try when the URL does not have one. For example, ['html'] makes /about try /about.html. |
index | string | 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:
- Maps the URL path to a file in the configured directory.
- If no file matches (and no
extensionsorindexfallback matches), callsnext()to pass the request to downstream middleware. - If a file matches:
- Generates an
ETagfrom the file content hash (ifetagis enabled). - Checks
If-None-MatchandIf-Modified-Sinceheaders. Returns304 Not Modifiedif the file has not changed. - Handles
Rangerequests for partial content (206 Partial Content). - Sets
Content-Typebased on the file extension. - Sets
Cache-Control,ETag,Last-Modified, andAccept-Rangesheaders. - Returns the file content as the response body.
- Generates an
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-Matchreceive a304response 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:
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:
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:
staticFiles('./public', {
extensions: ['html'],
})A request for /about will try ./public/about, then ./public/about.html.
Disable Directory Index
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:
import { compression } from 'remix/compression-middleware'
import { staticFiles } from 'remix/static-middleware'
let router = createRouter({
middleware: [
compression(),
staticFiles('./public'),
],
})Related
- Middleware --- How middleware works in Remix.
- compression-middleware --- Compressing static file responses.
- Deployment Guide --- Serving static files in production.