Serving Static Files

Deliver Assets, Set Cache Headers, and Handle SPA Fallback in Koa

Serving Static Files

Use koa-static and koa-send to serve CSS, images, and JavaScript bundles with correct caching headers, and add an SPA fallback that returns index.html for unmatched routes.

3 min read Level 2/5 #koa#data#static
What you'll learn
  • Mount koa-static to serve a public directory with max-age caching
  • Use koa-send to stream individual files with custom headers
  • Implement an SPA catch-all fallback so client-side routing works correctly

Koa does not serve static files out of the box. The koa-static middleware mounts a directory and handles ETag, Last-Modified, and Cache-Control headers for you.

Installation

npm install koa-static koa-send

Basic Static Middleware

import Koa from 'koa';
import serve from 'koa-static';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = new Koa();

app.use(
  serve(path.join(__dirname, 'public'), {
    maxAge: 60 * 60 * 24 * 7,   // 7 days in seconds
    gzip:   true,                // serve .gz pre-compressed files if present
    br:     true,                // serve .br brotli-compressed files if present
  })
);

Place this middleware before your router so file requests short-circuit before hitting route logic.

Streaming a Single File with koa-send

Use koa-send when you need fine-grained control over a specific file — useful for protected downloads or dynamic paths.

import send from 'koa-send';
import Router from '@koa/router';

const router = new Router();

router.get('/downloads/:name', async (ctx) => {
  const dir  = path.join(__dirname, 'protected');
  await send(ctx, ctx.params.name, {
    root:    dir,
    maxAge:  0,           // no caching for protected files
    setHeaders(res) {
      res.setHeader('Content-Disposition', `attachment; filename="${ctx.params.name}"`);
    },
  });
});

Cache-Control Strategy

Asset TypeRecommended max-ageNotes
Hashed bundles (app.abc123.js)31536000 (1 year)Safe to cache forever — hash changes on update
HTML files0 or no-cacheMust stay fresh so new hashes are picked up
Images (versioned)604800 (1 week)Balance freshness and bandwidth
API responses0Dynamic; set on the route, not static middleware

SPA Fallback

When a single-page app handles routing in the browser, the server must return index.html for any path that does not match a file or API route.

// Place AFTER the router and static middleware
app.use(async (ctx) => {
  await send(ctx, 'index.html', { root: path.join(__dirname, 'public') });
});

Because koa-static calls next() when no file is found, the SPA fallback only fires for unmatched requests — a correct and predictable cascade.

Up Next

Learn how to stream large files and binary data directly from Koa handlers without buffering the entire response in memory.

Streaming Responses →