Start
Scaffold an app, install, and run. One command creates the server, routes, and views.
bunx create-crumbun my-app
cd my-app
bun install
bun run devimport { serve } from "crumbun";
const server = await serve({ root: ".." });
console.log(`Crumbun running at http://${server.hostname}:${server.port}`);crumbuncreate-crumbunBun.serve()Routes
`src/api/**/page.ts` creates routes. Bracket folders become URL params. Parenthesised folders are route groups (dropped from the URL).
// src/api/story/[id]/page.ts
import type { PageContext } from "crumbun";
export function GET({ params, render }: PageContext) {
return render("story/story", { title: `Story ${params.id}` });
}// (group) folders are dropped from the URL
src/api/(marketing)/about/page.ts -> /about
src/api/blog/(v2)/[slug]/page.ts -> /blog/:slugGET({ params })POST({ request })(marketing)/aboutsrc/views/index.pugViews
Pug files live in `src/views`. `render("story/story")` renders `src/views/story/story.pug`. View CSS and SCSS are served from `/_crumbun`.
// src/api/story/[id]/page.ts
export function GET({ render }: PageContext) {
return render("story/story", { title: "Hi" });
}
// src/views/story/story.pug
h1= titlerender("story/story")/_crumbun/story/story.css/favicon.svghighlightCode(src, "ts")Layout
Create `src/views/_layout.pug` to wrap every view. The engine injects the view as `content`; emit it with `!= content`. Views that already `extends` are left alone.
// src/views/_layout.pug (wraps every view)
doctype html
html(lang="en")
body
!= content
// src/views/index.pug (no extends needed)
h1 Home_layout.pugrender(v, { layout: false })!= contentErrors
Create `src/views/_error.pug` to render 404 and other errors. It receives `status` and `message` locals. Without it, crumbun returns plain text.
// src/views/_error.pug (status + message locals)
p.error= message + " (" + status + ")"_error.pugAllow headerstatus, messageData
Handlers get `cookies`, `redirect`, and the `env` helper for the common request/response plumbing.
export function POST({ cookies, json }: PageContext) {
cookies.set("session", "abc", { httpOnly: true });
return json({ ok: true });
}export function GET({ request, redirect }: PageContext) {
if (!request.headers.get("authorization")) return redirect("/login", 307);
return redirect("/dashboard");
}import { env } from "crumbun";
const port = env("PORT", "3000");
const token = env("API_TOKEN");cookies.get(name)cookies.set(name, val, opts)env("PORT", "3000")Advanced
SPA fallback and asset caching harden real apps without extra dependencies. Both are opt-in.
import { serve } from "crumbun";
// unknown GETs render the root view for client routing
await serve({ root: "..", spa: true });// public files + /_crumbun CSS get:
// Cache-Control: public, max-age=3600
// ETag: "<size>-<mtime>"
// If-None-Match -> 304serve({ spa: true })If-None-Matchmax-age=3600Static
`exportStatic` renders selected routes and copies assets into `dist`.
import { fileURLToPath } from "node:url";
import { exportStatic } from "crumbun";
await exportStatic({
root: fileURLToPath(new URL("..", import.meta.url)),
paths: ["/", "/story/first-light"],
});dist/story/first-light/index.htmldist/_crumbun/style.cssdist/favicon.svgHandler
Handlers receive a tiny context and return Web responses or plain values.
import type { PageContext } from "crumbun";
export async function GET({ params, render }: PageContext) {
return render("story/story", {
title: `Story ${params.id}`,
});
}requestparamsurlrender(view, locals?, init?)json(value, init?)redirect(path, status?)cookieshighlightCode(src, lang)