Skip to Content
middlewarebuiltinServe Static

Last Updated: 3/9/2026


Serve Static Middleware

The Serve Static middleware enables serving static files such as images, CSS, JavaScript, and other assets from your Hono application. The middleware is runtime-specific and must be imported from the appropriate adapter.

Runtime-Specific Implementations

Serve Static middleware is not directly available from hono/serve-static. Instead, import it from your runtime-specific adapter:

Cloudflare Workers

import { serveStatic } from 'hono/cloudflare-workers'

Bun

import { serveStatic } from 'hono/bun'

Deno

import { serveStatic } from 'hono/deno'

Node.js

For Node.js, use the @hono/node-server package:

import { serveStatic } from '@hono/node-server/serve-static'

Basic Usage

Serve from a Directory

import { Hono } from 'hono' import { serveStatic } from 'hono/cloudflare-workers' const app = new Hono() app.use('/static/*', serveStatic({ root: './' })) app.use('/favicon.ico', serveStatic({ path: './favicon.ico' })) app.get('/', (c) => c.text('Hello Hono!'))

With this configuration:

  • Files in ./static/ are served at /static/*
  • ./favicon.ico is served at /favicon.ico

Serving from Public Directory

app.use('/public/*', serveStatic({ root: './' }))

This serves files from ./public/ directory:

  • ./public/images/logo.png/public/images/logo.png
  • ./public/styles/main.css/public/styles/main.css

Options

root

  • Type: string
  • Default: "./"
  • Description: The root directory to serve files from.
app.use('/assets/*', serveStatic({ root: './public' }))

path

  • Type: string
  • Description: Serve a specific file at a specific route.
app.use('/favicon.ico', serveStatic({ path: './public/favicon.ico' }))

rewriteRequestPath

  • Type: (path: string) => string
  • Description: Rewrite the request path before looking up the file.
app.use( '/static/*', serveStatic({ root: './', rewriteRequestPath: (path) => path.replace(/^\/static/, '/public'), }) )

This example serves files from ./public/ but makes them accessible at /static/:

  • Request: /static/image.png
  • Served: ./public/image.png

precompressed

  • Type: boolean
  • Default: false
  • Description: Serve precompressed files (.br, .zst, .gz) if available and the client supports them.
app.use( '/static/*', serveStatic({ root: './', precompressed: true, }) )

If you have these files:

  • ./static/app.js
  • ./static/app.js.br (Brotli compressed)
  • ./static/app.js.gz (Gzip compressed)

The middleware will automatically serve the compressed version based on the client’s Accept-Encoding header.

mimes

  • Type: Record<string, string>
  • Description: Custom MIME type mappings for file extensions.
app.use( '/static/*', serveStatic({ root: './', mimes: { m3u8: 'application/vnd.apple.mpegurl', ts: 'video/mp2t', }, }) )

onFound

  • Type: (path: string, c: Context) => void | Promise<void>
  • Description: Callback function executed when a file is found.
app.use( '/static/*', serveStatic({ root: './', onFound: (path, c) => { console.log(`Serving: ${path}`) }, }) )

onNotFound

  • Type: (path: string, c: Context) => void | Promise<void>
  • Description: Callback function executed when a file is not found.
app.use( '/static/*', serveStatic({ root: './', onNotFound: (path, c) => { console.log(`Not found: ${path}`) }, }) )

Runtime-Specific Features

Cloudflare Workers - Static Assets

Cloudflare Workers has a built-in Static Assets feature  that is recommended for serving static files. Configure it in wrangler.toml:

assets = { directory = "public" }

Directory structure:

. ├── package.json ├── public │ ├── favicon.ico │ └── static │ └── hello.txt ├── src │ └── index.ts └── wrangler.toml

Files are automatically served:

  • ./public/favicon.ico/favicon.ico
  • ./public/static/hello.txt/static/hello.txt

Cloudflare Workers - Manifest (Legacy)

For older Cloudflare Workers projects using KV-based static assets, you must provide a manifest:

import { serveStatic } from 'hono/cloudflare-workers' import manifest from '__STATIC_CONTENT_MANIFEST' app.use('/static/*', serveStatic({ root: './assets', manifest }))

Node.js - Serving from File System

import { serve } from '@hono/node-server' import { serveStatic } from '@hono/node-server/serve-static' import { Hono } from 'hono' const app = new Hono() app.use('/static/*', serveStatic({ root: './' })) app.use('/favicon.ico', serveStatic({ path: './public/favicon.ico' })) serve(app)

Bun - High-Performance File Serving

Bun’s native file I/O makes static file serving very fast:

import { Hono } from 'hono' import { serveStatic } from 'hono/bun' const app = new Hono() app.use('/static/*', serveStatic({ root: './' })) export default app

Deno - Import from npm

import { Hono } from 'jsr:@hono/hono' import { serveStatic } from 'jsr:@hono/hono/deno' const app = new Hono() app.use('/static/*', serveStatic({ root: './' })) Deno.serve(app.fetch)

Security Considerations

Path Traversal Protection

The middleware includes built-in protection against path traversal attacks. Requests containing .. in the path are automatically rejected:

// These requests will be rejected: // GET /static/../../../etc/passwd // GET /static/..%2F..%2Fetc%2Fpasswd
  1. Always specify a root directory: Don’t serve from the application root

    // Good app.use('/static/*', serveStatic({ root: './public' })) // Avoid app.use('/*', serveStatic({ root: './' }))
  2. Use specific path prefixes: Avoid serving all routes

    // Good app.use('/assets/*', serveStatic({ root: './public' })) // Risky app.use('/*', serveStatic({ root: './public' }))
  3. Separate static files from source code: Keep public assets in a dedicated directory

    project/ ├── src/ # Application code ├── public/ # Static assets └── package.json

Performance Tips

1. Use Precompression

Precompress your static assets during build time:

# Compress with Brotli brotli -k public/app.js # Creates public/app.js.br # Compress with Gzip gzip -k public/app.js # Creates public/app.js.gz

Then enable precompressed serving:

app.use('/static/*', serveStatic({ root: './', precompressed: true }))

2. Set Cache Headers

Combine with other middleware to set caching headers:

import { serveStatic } from 'hono/cloudflare-workers' import { cache } from 'hono/cache' // Cache static assets for 1 year app.use( '/static/*', cache({ cacheName: 'static-assets', cacheControl: 'public, max-age=31536000, immutable', }) ) app.use('/static/*', serveStatic({ root: './' }))

3. Use CDN for Production

For production applications, consider serving static files through a CDN:

  • Cloudflare Workers: Use Static Assets or R2 + CDN
  • Vercel: Automatic CDN for public/ directory
  • AWS: S3 + CloudFront

Examples

Complete Static Site

import { Hono } from 'hono' import { serveStatic } from 'hono/cloudflare-workers' const app = new Hono() // Serve static assets app.use('/assets/*', serveStatic({ root: './' })) // Serve index.html for root app.get('/', serveStatic({ path: './public/index.html' })) // API routes app.get('/api/hello', (c) => c.json({ message: 'Hello!' })) export default app

SPA with Fallback

import { Hono } from 'hono' import { serveStatic } from 'hono/bun' const app = new Hono() // Serve static files app.use('/assets/*', serveStatic({ root: './dist' })) // API routes app.get('/api/*', (c) => c.json({ api: true })) // Fallback to index.html for SPA routing app.get('*', serveStatic({ path: './dist/index.html' })) export default app

Multiple Static Directories

import { Hono } from 'hono' import { serveStatic } from 'hono/bun' const app = new Hono() // Serve images app.use('/images/*', serveStatic({ root: './public' })) // Serve CSS app.use('/css/*', serveStatic({ root: './public' })) // Serve JavaScript app.use('/js/*', serveStatic({ root: './public' })) // Serve uploads from different directory app.use('/uploads/*', serveStatic({ root: './storage' })) export default app

Custom 404 Handler

import { Hono } from 'hono' import { serveStatic } from 'hono/bun' const app = new Hono() app.use( '/static/*', serveStatic({ root: './', onNotFound: (path, c) => { console.log(`File not found: ${path}`) }, }) ) // Custom 404 page app.notFound((c) => { return c.html('<h1>404 Not Found</h1>', 404) }) export default app

Troubleshooting

Files Not Being Served

  1. Check the root path: Ensure the root directory is correct relative to your entry point

    // If your structure is: // project/ // src/index.ts // public/file.txt // Use: serveStatic({ root: './public' }) // from project root
  2. Verify file permissions: Ensure the runtime has read access to the files

  3. Check route pattern: Make sure the URL pattern matches

    app.use('/static/*', serveStatic({ root: './public' })) // Serves: /static/file.txt from ./public/static/file.txt

MIME Type Issues

If files are served with incorrect MIME types, provide custom mappings:

app.use( '/static/*', serveStatic({ root: './', mimes: { wasm: 'application/wasm', json: 'application/json', }, }) )

Cloudflare Workers Specific Issues

  1. Module Worker vs Service Worker: The new Static Assets feature only works with Module Worker mode (using export default app)

  2. Manifest errors: If using legacy KV-based static assets, ensure the manifest is imported:

    import manifest from '__STATIC_CONTENT_MANIFEST' serveStatic({ root: './', manifest })