Static Files
Serve CSS, images, fonts, and other static assets with config-driven middleware.
Overview
The @tekir/static package provides static file serving middleware. When registered, it intercepts every GET and HEAD request, looks for a matching file under the configured directory, and responds with the correct Content-Type, cache headers, and conditional-request support. If no file is found the middleware calls next() so your route handlers take over.
Installation
bun add @tekir/staticConfiguration
Create config/static.ts and export a plain object. The StaticProvider reads this file at boot and registers the middleware automatically.
import type { StaticConfig } from '@tekir/static'
export default {
dir: 'public',
index: 'index.html',
maxAge: 0,
immutable: false,
etag: true,
dotFiles: 'ignore'
} satisfies StaticConfig- dir: directory to serve files from, relative to
process.cwd(). Default:"public". - index: file to serve for directory requests. Default:
"index.html". - maxAge:
Cache-Control max-agein seconds. Default:0. - immutable: append
, immutableto Cache-Control. Default:false. - etag: generate and check
ETagheaders. Default:true. - dotFiles: how to handle dot files:
"ignore","deny", or"allow". Default:"ignore".
A production example with caching and dot-file protection:
import env from '#env'
import type { StaticConfig } from '@tekir/static'
export default {
dir: 'public',
maxAge: env.NODE_ENV === 'production' ? 86400 : 0,
immutable: false,
etag: true,
dotFiles: 'deny'
} satisfies StaticConfigBasic Usage
Register StaticProvider in your kernel. It reads config/static.ts and registers the middleware globally.
import type { TekirApp } from '@tekir/core'
import { StaticProvider } from '@tekir/static'
export default function({ app }: TekirApp) {
app.registerAll([StaticProvider])
}// That's it, requests like /logo.png now map to public/logo.png.
// No manual middleware registration needed.frontend: { type: 'vite' } or frontend: { type: 'next' }), static files are served by the frontend dev server in development and from the build output in production. You do not need @tekir/static in that case.Cache Control
When maxAge is greater than zero, the middleware sets a Cache-Control: public, max-age=N header. For versioned assets with content hashes set immutable: true to tell browsers they never need to revalidate.
// config/static.ts
export default {
dir: 'public',
maxAge: 86400 // 1 day
} satisfies StaticConfig
// Sends: Cache-Control: public, max-age=86400// config/static.ts, long-lived assets with content hashes
export default {
dir: 'dist/assets',
maxAge: 31536000, // 1 year
immutable: true
} satisfies StaticConfig
// Sends: Cache-Control: public, max-age=31536000, immutableETag Support
ETag support is enabled by default. The tag is derived from the file's byte size and last-modified timestamp. On repeat visits the browser sends the tag back in an If-None-Match header. If the file has not changed the server replies with 304 Not Modified and skips the file body.
// ETag is enabled by default. The server computes the tag from
// the file's size and mtime:
// ETag: "42381-1711234567890"
//
// On subsequent requests the browser sends:
// If-None-Match: "42381-1711234567890"
//
// If the file hasn't changed, the server replies 304 Not Modified.
// Disable ETags if you manage caching entirely via Cache-Control:
export default {
etag: false
} satisfies StaticConfigDot Files
Files whose names begin with a dot are treated specially. The dotFiles option controls the behavior:
// 'ignore' (default), dot files are skipped; the next middleware handles the request
export default { dotFiles: 'ignore' } satisfies StaticConfig
// 'deny': dot files return 403 Forbidden
export default { dotFiles: 'deny' } satisfies StaticConfig
// 'allow': dot files are served like any other file
export default { dotFiles: 'allow' } satisfies StaticConfig- ignore (default): the middleware passes the request to the next handler.
- deny: responds with
403 Forbidden. Recommended for production to protect files like.env. - allow: dot files are served like any other file.
Manual Usage
If you prefer not to use the provider, call serveStatic() directly in your kernel. This gives you full control over the middleware registration:
import type { TekirApp } from '@tekir/core'
import { serveStatic } from '@tekir/static'
export default function({ router }: TekirApp) {
router.useGlobal([serveStatic({
dir: 'public',
maxAge: 3600,
etag: true,
dotFiles: 'ignore'
})])
}Supported MIME Types
The middleware sets the correct Content-Type based on the file extension:
- Web:
.html,.css,.js,.mjs,.json,.xml,.txt - Images:
.png,.jpg,.jpeg,.gif,.svg,.ico,.webp,.avif - Fonts:
.woff,.woff2,.ttf,.otf - Media:
.mp4,.webm,.mp3,.wav - Other:
.pdf,.zip,.wasm,.map
Unknown extensions fall back to application/octet-stream.