crumbun

tiny Bun fullstack engine · file routes · Pug views · static export

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 dev
import { serve } from "crumbun";

const server = await serve({ root: ".." });
console.log(`Crumbun running at http://${server.hostname}:${server.port}`);
01enginecrumbun
02scaffold CLIcreate-crumbun
03runtimeBun.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/:slug
GETexport a GET handlerGET({ params })
POSTexport any HTTP methodPOST({ request })
(grp)pathless folder(marketing)/about
rootno match renders indexsrc/views/index.pug

Views

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= title
pugrender a viewrender("story/story")
cssview stylesheet URL/_crumbun/story/story.css
pubpublic assets/favicon.svg
hlcode highlightinghighlightCode(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
wrapevery rendered view_layout.pug
skipopt out per viewrender(v, { layout: false })
injectposition the view!= content

Errors

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 + ")"
404missing route_error.pug
405method not allowedAllow header
msglocals passed instatus, message

Data

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");
getread incoming cookiecookies.get(name)
setattach to responsecookies.set(name, val, opts)
envBun.env with fallbackenv("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 -> 304
spaunknown GET -> indexserve({ spa: true })
etag304 on matchIf-None-Match
cachepublic assetsmax-age=3600

Static

`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"],
});
htmlrendered pagesdist/story/first-light/index.html
cssview stylesdist/_crumbun/style.css
pubpublic assetsdist/favicon.svg

Handler

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}`,
  });
}
ctxincoming Requestrequest
ctxroute params from [brackets]params
ctxparsed URLurl
ctxrender src/views/<view>.pugrender(view, locals?, init?)
ctxJSON responsejson(value, init?)
ctx302 (or status) redirectredirect(path, status?)
ctxget / set / delete / allcookies
ctxescaped, classed HTMLhighlightCode(src, lang)