Serve Files
The Serve Files middleware handles serving static files (HTML, CSS, JavaScript, images) from a directory on your server. It includes built-in path traversal protection, automatic index.html serving for directories, and intelligent file extension handling.
Quick Start
import { MageApp } from "@mage/app";
import { serveFiles } from "@mage/app/serve-files";
const app = new MageApp();
// Serve files from the public directory
app.get(
"/static/*",
serveFiles({
directory: "./public",
}),
);
// Requests to /static/style.css serve ./public/style.css
// Requests to /static/ serve ./public/index.html (if it exists)
Deno.serve(app.handler);
How It Works
Serves files from a directory on GET/HEAD wildcard routes. Checks files in
order: exact match, directory index (index.html if enabled), .html
extension. Prevents path traversal by validating resolved paths stay within the
configured directory. Returns 405 for non-GET/HEAD methods.
Options
| Option | Type | Default | Description |
|---|---|---|---|
directory |
string |
(required) | Directory to serve files from |
serveIndex |
boolean |
true |
Serve index.html when a directory is requested |
onNotFound |
(c: MageContext) => void | Promise |
c.notFound() |
Custom handler when file is not found |
directory
The filesystem path to serve files from. This should be a relative or absolute path:
// Relative path (relative to current working directory)
app.get(
"/static/*",
serveFiles({
directory: "./public",
}),
);
// Absolute path (recommended for serverless)
import { resolve } from "@std/path";
app.get(
"/static/*",
serveFiles({
directory: resolve(Deno.cwd(), "public"),
}),
);
For serverless environments and production deployments, use absolute paths to ensure consistency across different execution contexts.
serveIndex
When true (the default), the middleware serves index.html for directory
requests. Set to false to disable this behavior:
// serveIndex: true (default)
app.get(
"/docs/*",
serveFiles({
directory: "./docs",
serveIndex: true, // This is the default
}),
);
// Request: GET /docs/
// Serves: ./docs/index.html (if it exists)
// No file? → 404
// serveIndex: false
app.get(
"/files/*",
serveFiles({
directory: "./files",
serveIndex: false,
}),
);
// Request: GET /files/
// Result: 404 (directories never served)
onNotFound
Customize what happens when a file is not found. By default, calls
c.notFound(). Use this to serve custom 404 pages or implement fallback logic:
// Serve a custom 404 page
const notFoundHtml = await Deno.readTextFile("./public/404.html");
app.get(
"/*",
serveFiles({
directory: "./dist",
onNotFound: (c) => {
c.html(notFoundHtml, 404);
},
}),
);
For SPAs, you might want to serve the index page for all unmatched routes:
const indexHtml = await Deno.readTextFile("./dist/index.html");
app.get(
"/*",
serveFiles({
directory: "./dist",
onNotFound: (c) => {
// Let client-side router handle the route
c.html(indexHtml, 200);
},
}),
);
The handler can be async:
app.get(
"/*",
serveFiles({
directory: "./dist",
onNotFound: async (c) => {
const html = await Deno.readTextFile("./errors/404.html");
c.html(html, 404);
},
}),
);
Examples
Serve a Static Website
import { MageApp } from "@mage/app";
import { serveFiles } from "@mage/app/serve-files";
const app = new MageApp();
// Serve all static files from the dist directory
app.get(
"/static/*",
serveFiles({
directory: "./dist",
}),
);
// You might also serve from the root
app.get(
"/*",
serveFiles({
directory: "./dist",
}),
);
Deno.serve(app.handler);
Requests like /static/index.html, /static/images/logo.png, and
/static/css/style.css all work correctly.
Serve a Single-Page Application
For SPAs, you typically want to serve index.html for all HTML routes and let
the client-side router handle navigation:
import { MageApp } from "@mage/app";
import { serveFiles } from "@mage/app/serve-files";
const app = new MageApp();
// Serve static assets with their proper extensions
app.get(
"/assets/*",
serveFiles({
directory: "./dist/assets",
}),
);
// Serve HTML routes by appending .html
// Request: GET /dashboard → serves ./dist/dashboard.html
app.get(
"/*",
serveFiles({
directory: "./dist",
}),
);
Deno.serve(app.handler);
With this setup:
/assets/app.jsserves the JavaScript bundle/dashboardserves./dist/dashboard.html/serves./dist/index.html(directory index)/admin/settingsserves./dist/admin/settings.html
Serve User Uploads
Serve user-generated files with path traversal protection:
import { MageApp } from "@mage/app";
import { serveFiles } from "@mage/app/serve-files";
const app = new MageApp();
// Serve uploaded files
app.get(
"/uploads/*",
serveFiles({
directory: "./uploads",
serveIndex: false, // Don't list directories
}),
);
// Even if an attacker tries ../../../etc/passwd, it's blocked
// Request: GET /uploads/../../etc/passwd
// Result: 404 Not Found
Deno.serve(app.handler);
Serve Documentation
import { MageApp } from "@mage/app";
import { serveFiles } from "@mage/app/serve-files";
const app = new MageApp();
// Serve documentation with automatic index.html
app.get(
"/docs/*",
serveFiles({
directory: "./docs",
serveIndex: true,
}),
);
Deno.serve(app.handler);
Requests work intuitively:
/docs/serves./docs/index.html/docs/guidesserves./docs/guides/index.html/docs/guides/getting-startedserves./docs/guides/getting-started.html
Security Considerations
Path traversal protection: Built-in. Validates all paths stay within configured directory, even with URL encoding or symbolic links.
Important:
- Don't serve project root—only serve specific directories
- Middleware respects filesystem permissions (unreadable = 404)
- No directory listing (returns 404 unless
index.htmlexists) - Correct
Content-Typeheaders set automatically
Notes
- Must use wildcard routes (e.g.,
/static/*) - GET and HEAD only (other methods return 405)
- For large files, use CDN or nginx in production
Common Patterns
Combine with Cache Control
import { MageApp } from "@mage/app";
import { serveFiles } from "@mage/app/serve-files";
import { cacheControl } from "@mage/app/cache-control";
const app = new MageApp();
// Cache static assets for a long time
app.get(
"/assets/*",
cacheControl({
public: true,
maxAge: 31536000,
immutable: true,
}),
serveFiles({
directory: "./dist/assets",
}),
);
Deno.serve(app.handler);
Serve Multiple Directories
import { MageApp } from "@mage/app";
import { serveFiles } from "@mage/app/serve-files";
const app = new MageApp();
// Serve assets from dist
app.get(
"/assets/*",
serveFiles({
directory: "./dist/assets",
}),
);
// Serve public files
app.get(
"/public/*",
serveFiles({
directory: "./public",
}),
);
// Serve downloads
app.get(
"/downloads/*",
serveFiles({
directory: "./downloads",
serveIndex: false,
}),
);
Deno.serve(app.handler);
Conditional Serving
import { MageApp } from "@mage/app";
import { serveFiles } from "@mage/app/serve-files";
const app = new MageApp();
const directory = Deno.env.get("NODE_ENV") === "production"
? "./dist"
: "./public";
app.get("/*", serveFiles({ directory }));
Deno.serve(app.handler);
Related
- Middleware System - How middleware works in Mage
- MageContext - Request and response context
- Cache Control - Add caching headers to responses
- CORS - Handle cross-origin requests for static assets