Getting Started
Create a Furin application with Bun:
bun create furin@latest my-app
cd my-app
bun run dev
The interactive scaffolder offers two templates:
| Template | Includes |
|---|---|
simple | Tailwind CSS and an Elysia API route |
full | The simple stack plus shadcn/ui components |
Choose one without a prompt when scripting:
bun create furin@latest my-app --template simple --yes
Project Structure
Both templates produce the same framework shape:
my-app/
├── public/
│ └── favicon.ico
├── src/
│ ├── api/
│ │ └── hello.ts
│ ├── pages/
│ │ ├── globals.css
│ │ ├── index.tsx
│ │ └── root.tsx
│ └── server.ts
├── bunfig.toml
├── furin.config.ts
├── furin-env.d.ts
├── README.md
└── tsconfig.json
src/server.tscomposes the Elysia application.src/pages/root.tsxowns the HTML document shared by every page.src/pages/index.tsxmaps to/.furin.config.tscontrols production builds.bunfig.tomlregisters the client-side Bun plugins used by development.furin-env.d.tsis generated from the route tree and should not be edited manually. Despite its name, it does not type environment variables: it registers every page'srouteexport so<Link to>,navigate(), route params, andgetRouteApi()are type-checked, and references Furin's ambient types for CSS and image imports. The dev server rewrites it when it starts and when pages change;furin buildandfurin typegenwrite it too.
The Server
Furin is an Elysia plugin, so pages and APIs compose through .use():
import { Elysia } from "elysia";
import { furin } from "@teyik0/furin";
import { api } from "./api/hello";
export const port = Number(process.env.PORT ?? 3000);
const app = new Elysia()
.use(await furin({ pagesDir: "./src/pages" }))
.use(api);
if (import.meta.main) {
app.listen(port);
console.log(`Furin app running at http://localhost:${app.server?.port}`);
}
export default app;
Keep chaining the returned Elysia instance. Plugin order and lifecycle scope remain Elysia concerns; see API Routes.
The Root Document
Every pages directory needs root.tsx. It must render a complete document containing <HeadContent /> and <Scripts />:
import { defineRootRoute, HeadContent, Scripts } from "@teyik0/furin";
import "./globals.css";
export const route = defineRootRoute()
.config({ mode: "ssg" })
.layout(({ children }) => (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
));
<HeadContent /> renders framework assets and the matched page's metadata. <Scripts /> serializes route state and loads the browser entry.
Your First Page
Every non-root route passes its file's id to defineRoute(id). The id gives the route its parent layout's types; see Route Ids:
import { defineRoute } from "@teyik0/furin";
export const route = defineRoute("/")
.config({ mode: "ssg" })
.head(() => ({ meta: [{ title: "Home" }] }))
.page(() => <h1>Hello, Furin</h1>);
Create a route file empty and the development server writes a starter route with its id. Furin never rewrites a file that has content: a missing or stale id is reported in the terminal and the error overlay, with the exact defineRoute(...) call to write.
Development And Production
# Bun dev server with React Fast Refresh
bun run dev
# Check formatting and types, then build
bun run fix
bun run tscheck
bun run build
# Run the built Bun server
bun run start
The scaffold's dev script starts src/server.ts with Bun hot reloading. If you add, remove, or move a page, the route manifest and furin-env.d.ts are refreshed automatically.
Next, read File-Based Routing, then Data Loading.