Serving Static Files
Serve static assets, SPAs, and React applications using Thunder's built-in serveAssets utility.
Thunder is not limited to JSON APIs. It includes a built-in serveAssets utility that allows you to serve any static content - HTML pages, CSS, JavaScript bundles, images, and even fully built React or SPA applications - directly from your Thunder server.
Project Structure
Place your static files inside the public/ directory of your Thunder project:
Creating a Static Route
Scaffold the Route
Inside your router file, use the req-static snippet to generate a pre-configured static handler, or generate it manually using the req snippet and modify it as shown below.
Define the Wildcard Pattern
The route path uses a wildcard parameter to capture any sub-path beneath the mount point. This is powered by path-to-regexp:
import { Router } from "@/core/http/router.ts";
import { serveAssets } from "@/core/http/assets.ts";
import { paramsAsJson } from "@/core/http/utils.ts";
import { fromFileUrl } from "https://deno.land/std/path/mod.ts";
import z from "zod";
export default new Router("/", function index(router) {
router.get("{/*endpoint}", function index() {
const $params = z.object({
endpoint: z.array(z.string()).optional(),
});
return (req: Request) => {
const { endpoint } = $params.parse(paramsAsJson(req));
return serveAssets(
req,
fromFileUrl(import.meta.resolve("../public/www")),
endpoint?.join("/"),
);
};
});
});How It Works
| Parameter | Description |
|---|---|
req | The incoming Request object |
root | The absolute path to the directory containing your static files |
filePath | The relative file path derived from the URL, used to locate the specific asset |
The {/*endpoint} pattern captures everything after the base path as an array of path segments. For example:
| Request URL | endpoint value |
|---|---|
/ | undefined (serves index.html) |
/app.js | ["app.js"] |
/assets/logo.png | ["assets", "logo.png"] |
Serving a React Application (Complete Example)
Let's say you have built a React Single Page Application (SPA) for an internal dashboard and you want Thunder to serve it. We will call this the Monitor App.
Create the Public Folder
First, build your React app and place its compiled output inside the public/monitor/ folder of your Thunder project:
Create the Route
Next, create a dedicated route for the monitor application. Thunder will serve "index.html" as a fallback if a specific asset isn't found, which makes client-side routing in React work seamlessly:
import { Router } from "@/core/http/router.ts";
import { serveAssets } from "@/core/http/assets.ts";
import { paramsAsJson } from "@/core/http/utils.ts";
import { fromFileUrl } from "https://deno.land/std/path/mod.ts";
import z from "zod";
export default new Router("/monitor", function monitorApp(router) {
router.get("{/*endpoint}", function index() {
const $params = z.object({
endpoint: z.array(z.string()).optional(),
});
return (req: Request) => {
const { endpoint } = $params.parse(paramsAsJson(req));
// Point serveAssets to the public/monitor directory
return serveAssets(
req,
fromFileUrl(import.meta.resolve("../public/monitor")),
endpoint?.join("/"),
);
};
});
});This pattern works seamlessly with any framework that produces a static build output, including React, Vue, Svelte, and SolidJS.
Important Note
Unlike regular route handlers that return a shape/handler object, static handlers return a function directly:
// Regular API handler
return {
shape: () => ({ ... }),
handler: async (req) => { ... },
};
// Static file handler - returns a function directly
return (req: Request) => {
return serveAssets(...);
};Remember to use the @/ import alias when importing serveAssets and other framework utilities. Relative imports into core/ are forbidden.