The Frontend
Serve a static single-page Todo UI directly from your Thunder server.
Thunder isn't just for JSON APIs - it can serve your static frontend too. We'll add a small HTML + JavaScript single-page app that talks to the Todo API we built, and serve it with serveAssets. See Serving Static Files for the full picture.
Add the Static Files
Place your frontend in public/www:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Thunder Todos</title>
</head>
<body>
<h1>Todos</h1>
<form id="new-todo">
<input id="title" placeholder="What needs doing?" required />
<button type="submit">Add</button>
</form>
<ul id="list"></ul>
<script src="/app.js"></script>
</body>
</html>const list = document.getElementById("list");
const form = document.getElementById("new-todo");
async function load() {
const res = await fetch("/todos");
const { results } = await res.json();
list.innerHTML = results
.map((t) => `<li>${t.completed ? "✅" : "⬜"} ${t.title}</li>`)
.join("");
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
const title = document.getElementById("title").value;
await fetch("/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
form.reset();
load();
});
load();Create the Static Route
Add a fallback router that serves the SPA from public/www. The {/*endpoint} wildcard captures any sub-path, and serveAssets falls back to index.html so client-side routing works:
import { Router } from "@/core/http/router.ts";
import { paramsAsJson, serveAssets } from "@/core/http/utils.ts";
import { fromFileUrl } from "@std/path/from-file-url";
import z from "zod";
export default new Router("/", function index(router) {
router.get("{/*endpoint}", function staticFiles() {
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("/"),
);
};
});
});Static handlers return a function directly instead of a shape/handler object. See the note on static handlers.
Try It Out
Run deno task dev and open http://localhost:8000. You should see your Todo UI, fully wired to the API.
That's a complete Thunder application - model, validated API, pagination, and a frontend - all in a handful of files.
Want per-user todos with login? Install thunder-core and use createCRUD's isolationFields to scope every todo to the authenticated user.