Getting Started

Create a Furin application with Bun:

sh
bun create furin@latest my-app
cd my-app
bun run dev

The interactive scaffolder offers two templates:

TemplateIncludes
simpleTailwind CSS and an Elysia API route
fullThe simple stack plus shadcn/ui components

Choose one without a prompt when scripting:

sh
bun create furin@latest my-app --template simple --yes

Project Structure

Both templates produce the same framework shape:

text
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.ts composes the Elysia application.
  • src/pages/root.tsx owns the HTML document shared by every page.
  • src/pages/index.tsx maps to /.
  • furin.config.ts controls production builds.
  • bunfig.toml registers the client-side Bun plugins used by development.
  • furin-env.d.ts is generated from the route tree and should not be edited manually. Despite its name, it does not type environment variables: it registers every page's route export so <Link to>, navigate(), route params, and getRouteApi() 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 build and furin typegen write it too.

The Server

Furin is an Elysia plugin, so pages and APIs compose through .use():

src/server.ts
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 />:

src/pages/root.tsx
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:

src/pages/index.tsx
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

sh
# 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.

Comments