# Buntal JS: complete reference for AI agents > Buntal JS is an ultra-lightweight, type-safe full-stack framework for Bun and React with Next.js-like file-system routing. This file is the whole API in one place. All packages share one version (see npm). Requires Bun >= 1.2. ## Ground rules - Buntal is NOT Next.js. Never import from `next/*`. There is no App Router, no server components, no `"use client"`, no `getServerSideProps`, no `generateMetadata`. Use the APIs below only. - Runtime is Bun. Prefer Bun built-ins: `Bun.SQL` (database), `Bun.markdown`, `Bun.password`, `Bun.file`, `Bun.write`, `bun test`. - TypeScript everywhere. `.tsx` for pages and layouts, `.ts` for API routes. ## Packages | Package | Purpose | | --- | --- | | `buntal` | React framework: pages, layouts, `Link`, `Meta`, `Script`, `Svg`, `useRouter`, `BuntalConfig` | | `@buntal/http` | HTTP server: `Http`, `h`, `Req`, `Res`, `Cookie` | | `@buntal/http/middlewares` | `auth`, `jwt`, `cors`, `logger`, `secureHeaders` | | `@buntal/cli` | `buntal dev`, `buntal build`, `buntal start` | | `create-buntal` | `bun create buntal@latest my-app` | ## Full-stack project ```sh bun create buntal@latest my-app # interactive template picker bun create buntal@latest my-app --template landing # landing page / portfolio bun create buntal@latest my-app --template blog # Markdown blog with a SQL database # use --template, not -t: `bun create` consumes short flags itself cd my-app bun dev # http://localhost:3000 (PORT env overrides) bun run build && bun start # production ``` Structure: ``` app/ layout.tsx root layout: renders , , index.tsx page for / globals.css Tailwind CSS v4 entry (compiled to /globals.css) favicon.svg about/index.tsx page for /about posts/[id]/index.tsx dynamic segment -> params.id docs/[[...slug]]/index.tsx optional catch-all -> params.slug ("a/b") api/hello/index.ts API route for /api/hello 404.tsx optional custom not-found page public/ static files served as-is (dotfiles are not served, except .well-known/) buntal.config.ts optional config ``` Rules: - A page is `app//index.tsx` with a default export. Folders starting with `_` are ignored. - `layout.tsx` wraps every page in its folder and below. A folder's layout only renders when that folder has a page. - The `@/` import alias maps to the project root (see tsconfig). - Import text files with `import Logo from '@/app/logo.svg' with { type: 'text' }`. ### Root layout ```tsx import { Meta, type MetaProps } from 'buntal' export default function RootLayout({ children, data }: Readonly<{ children: React.ReactNode; data?: { _meta?: MetaProps } }>) { return ( {children} ) } ``` ### Pages and props ```tsx export default function Page({ params, query, data }: { params: Record query: Record data?: unknown }) { return

Hello {query.name ?? 'there'}

} ``` ### `$`: server data loader Export `$` from a page or layout. It runs on the server for the initial render and is re-fetched as JSON on client navigation. The page receives the return value as `data`. `data._meta` sets SEO tags (merged with layout `_meta`, page wins). ```tsx import type { Req } from '@buntal/http' import type { MetaProps } from 'buntal' export const $ = async (req: Req) => { const post = await getPost(req.params.id) if (!post) return { notFound: true } return { post, _meta: { title: post.title, description: post.excerpt } satisfies MetaProps } } export default function PostPage({ data }: { data?: Awaited> }) { if (!data || 'notFound' in data) return

Not found

return
{data.post.title}
} ``` - Static data: `export const $ = { _meta: { title: 'About' } }` (an object, not a function) is inlined at build time. - Return a `Response` to stop rendering, e.g. a redirect: `return Response.redirect('/login', 302)` or `new Response(null, { status: 302, headers: { location: '/login' } })`. Works on full loads and client navigation. - `$` is removed from the browser bundle together with helpers and imports only it uses, so it can import database clients, `bun:*` modules and secrets. Do not read `$` as a value from the component. - Loader responses are sent with `Cache-Control: private, no-store`. In production, thrown errors return a generic 500 without details. - Validate `req.params` before touching the filesystem. Resolve paths and check they stay inside your content folder. ### API routes inside a full-stack app ```ts // app/api/todos/index.ts import { h } from '@buntal/http' export const GET = h((req, res) => res.json({ todos: [] })) export const POST = h(async (req, res) => { const body = (await req.json().catch(() => null)) as { title?: unknown } | null if (!body || typeof body.title !== 'string' || !body.title.trim()) { return res.status(400).json({ error: 'title is required' }) } return res.status(201).json({ title: body.title.trim() }) }) ``` Exports named GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD are dispatched. HEAD falls back to GET. Any other method gets 404. ### buntal.config.ts ```ts import { auth, secureHeaders } from '@buntal/http/middlewares' import type { BuntalConfig } from 'buntal' export default { appDir: './app', // default outDir: '.buntal', // default; must stay inside the project staticDir: './public', // default config: {}, // extra Bun.build options for the client bundle serverOptions: {}, // passed to Bun.serve middlewares: [secureHeaders()] } satisfies BuntalConfig ``` `middlewares` run before every page, `$` loader and API route. Static files and bundles are served before middlewares, so a login page still gets its JS and CSS. A global `auth()` therefore protects all pages; for public pages, protect per route instead (see Security). Environment: only variables named `BUNTAL_PUBLIC_*` are inlined into the browser bundle. Everything else stays on the server. ### Components and hooks (from `buntal`) - ``: client-side navigation for same-origin links. External, `mailto:`, modifier-clicks, `target="_blank"` and `download` behave like a normal ``. `href="#id"` smooth-scrolls (`#id:120` uses a 120px offset). `href="-1"` goes back. - ``: head tags. - `