Skip to content

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

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.html

Step 2: Add the Static Files Middleware

ts
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.css serves ./public/css/style.css
  • GET /images/logo.png serves ./public/images/logo.png
  • GET / serves ./public/index.html (the default index file)
  • GET /api/hello passes 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:

ts
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:

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

Now the middleware tries these paths in order for a request to /about:

  1. ./public/about (exact match)
  2. ./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:

ts
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:

ts
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:

ts
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

sh
# 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.css

Replace "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

Released under the MIT License.