Tutorial: Serve Static Files
In this tutorial you will serve static files from a directory, configure caching policies for different file types, enable clean URLs with extension fallbacks, and combine static serving with compression.
Prerequisites
- A Remix project with a working server (see the Getting Started guide).
Step 1: Create a Public Directory
Create a public directory in your project root and add some files:
public/
css/
style.css
js/
app.js
images/
logo.png
index.htmlStep 2: Add the Static Files Middleware
import { createRouter } from 'remix/fetch-router'
import { staticFiles } from 'remix/static-middleware'
import { route } from 'remix/fetch-router/routes'
let routes = route({
api: '/api/hello',
})
let router = createRouter({
middleware: [
staticFiles('./public'),
],
})
router.get(routes.api, () => {
return Response.json({ message: 'Hello from the API' })
})Now:
GET /css/style.cssserves./public/css/style.cssGET /images/logo.pngserves./public/images/logo.pngGET /serves./public/index.html(the default index file)GET /api/hellopasses through to the route handler
Step 3: Configure Cache-Control Headers
By default, files are served with Cache-Control: public, max-age=0, meaning the browser always revalidates. For production, set up smarter caching:
staticFiles('./public', {
cacheControl(path) {
// Hashed filenames (e.g., app-abc123.js) can be cached forever
if (path.includes('/assets/')) {
return 'public, max-age=31536000, immutable'
}
// Images change rarely
if (path.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) {
return 'public, max-age=86400' // 24 hours
}
// Everything else gets a short cache
return 'public, max-age=60'
},
})The function receives the file path (relative to the public directory) and returns a Cache-Control header value.
Step 4: Enable Clean URLs
If you want /about to serve ./public/about.html, configure extension fallbacks:
staticFiles('./public', {
extensions: ['html'],
})Now the middleware tries these paths in order for a request to /about:
./public/about(exact match)./public/about.html(extension fallback)
Step 5: Disable Directory Index
By default, requests for / serve ./public/index.html. If you want your route handler to handle the root URL instead:
staticFiles('./public', {
index: false,
})Now GET / passes through to your route handlers, and only explicit file paths are served as static files.
Step 6: Combine with Compression
Place the compression middleware before staticFiles so responses are compressed on the fly:
import { compression } from 'remix/compression-middleware'
import { staticFiles } from 'remix/static-middleware'
let router = createRouter({
middleware: [
compression(),
staticFiles('./public'),
],
})A request for /css/style.css from a browser that supports gzip will receive a compressed response. The Content-Encoding and Vary headers are set automatically.
Step 7: Full Production Configuration
Putting it all together:
import { createRouter } from 'remix/fetch-router'
import { logger } from 'remix/logger-middleware'
import { compression } from 'remix/compression-middleware'
import { staticFiles } from 'remix/static-middleware'
let router = createRouter({
middleware: [
logger(),
compression(),
staticFiles('./public', {
cacheControl(path) {
if (path.includes('/assets/')) {
return 'public, max-age=31536000, immutable'
}
return 'public, max-age=60'
},
extensions: ['html'],
}),
// ... other middleware and routes below
],
})Step 8: Verify Caching with curl
# First request -- returns 200 with ETag
curl -I http://localhost:3000/css/style.css
# Second request with ETag -- returns 304 Not Modified
curl -I -H 'If-None-Match: "abc123"' http://localhost:3000/css/style.cssReplace "abc123" with the actual ETag from the first response. You should see a 304 with no body.
What You Learned
- How to serve static files from a directory.
- How to set different cache policies for different file types.
- How to enable clean URLs with extension fallbacks.
- How to combine static file serving with compression.
- How to verify caching behavior.
Next Steps
- API Reference -- Full details on options and behavior.
- compression-middleware -- Compress static file responses.
- Middleware -- How middleware works in Remix.