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.icois 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.tomlFiles 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 appDeno - 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%2FpasswdRecommended Practices
-
Always specify a root directory: Don’t serve from the application root
// Good app.use('/static/*', serveStatic({ root: './public' })) // Avoid app.use('/*', serveStatic({ root: './' })) -
Use specific path prefixes: Avoid serving all routes
// Good app.use('/assets/*', serveStatic({ root: './public' })) // Risky app.use('/*', serveStatic({ root: './public' })) -
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.gzThen 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 appSPA 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 appMultiple 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 appCustom 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 appTroubleshooting
Files Not Being Served
-
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 -
Verify file permissions: Ensure the runtime has read access to the files
-
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
-
Module Worker vs Service Worker: The new Static Assets feature only works with Module Worker mode (using
export default app) -
Manifest errors: If using legacy KV-based static assets, ensure the manifest is imported:
import manifest from '__STATIC_CONTENT_MANIFEST' serveStatic({ root: './', manifest })
Related Documentation
- Cache Middleware - Add caching headers to static files
- Compress Middleware - Compress responses on-the-fly
- Getting Started - Cloudflare Workers - Cloudflare-specific static file serving
- Getting Started - Node.js - Node.js static file serving