Skip to content
At a glance
Start hereAt a glance
On this page

At a glance

A one-page overview of Goribu, from your first route to live app.

Serving pages

A Goribu app is a single Worker that streams server-rendered HTML on the first request. After hydration, client-side navigation fetches JSON to update the page and its data. A route’s handler runs on the server and its default export is the React page. Here is a complete route, using a small sample of data:

src/routes/posts/[id].jsx
// Find the post for the dynamic URL segment.
export function GET(req, res) {
  const posts = [
    { id: "42", title: "Hello Goribu", body: "Your first page." },
  ];
  const post = posts.find((post) => post.id === req.params.id);
  if (!post) return res.notFound();
  return res.render({ post });
}

// Render the handler’s data on the server and in the browser.
export default function PostPage({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

A page without a GET handler and other handlers gets pre-rendered and served from the CDN as static, for free. Its URL must be known at build time and no middleware wrapping it. Static pages still hydrate are interactive. Skip the page component for API-only routes.

CLI basics

Use Node.js 22+ and pnpm. Create an app, then install its dependencies from the new directory:

Terminal
pnpm create goribu@latest my-app
cd my-app
pnpm install
pnpm dev
CommandWhat it does
pnpm create-goribu my-appCreates a new Goribu TS app.
pnpm devStarts the local development server.
pnpm run deployBuilds and deploys the app to Cloudflare.

TypeScript

The examples on this page use JavaScript. New Goribu apps use TypeScript by default, with strict type checking configured. Use .ts for handlers and.tsx for pages that contain JSX. To create a JavaScript project, runpnpm create goribu@latest my-app --js.

To check your app's types run pnpm typecheck before deploying. Development and production builds compile TypeScript without running the type checker.

Goribu includes types for handlers, middleware, metadata, and forms. Use import type for types such as Handler and Middleware fromgoribu/server. Configuration stays in goribu.config.js in both JavaScript and TypeScript projects.

Routes

Routes live in src/routes/. The file’s path determines its URL. Anindex file serves its containing folder and square brackets capture a dynamic parameter.

FileURL
src/routes/index.jsx/
src/routes/about.jsx/about
src/routes/posts/index.jsx/posts
src/routes/posts/[id].jsx/posts/42, with req.params.id equal to "42"

Route files support .js, .jsx, .ts, and.tsx. Read path parameters from req.params and query strings with req.query.get("name").

Forms and req.body

<Form method="post"> submits to the current route’s POSThandler and gives you pending, errors, and datato display its state. After hydration, submissions update the form without reloading the page.

Set method to get, post, put,patch, or delete to call the corresponding handler in the route.

src/routes/greet.jsx
import { Form } from "goribu";

export default function Greet() {
  return (
    <Form method="post">
      {({ pending, errors, data }) => (
        <>
          <label>Name <input name="name" /></label>
          <p>{errors?.name}</p>
          <button disabled={pending}>Greet me</button>
          <p>{data?.greeting}</p>
        </>
      )}
    </Form>
  );
}

export async function POST(req, res) {
  const { name } = await req.body();
  if (typeof name !== "string" || !name.trim()) {
    return res.invalid({ name: "Enter your name." });
  }
  return res.json({ greeting: `Hello, ${name.trim()}!` });
}

req.body() is async, reads the submitted fields and parses JSON requests automatically. Return res.invalid() to show errors keyed by field name orres.json() to pass a successful result to data.

After a successful submission, return res.redirect("/posts") from the handler to navigate to another page. Add replace to<Form> if that navigation should replace the current browser history entry after hydration, such as when leaving a login page.

Client navigation

Use Link for client-side navigation. For the post route above:

JSX
import { Link } from "goribu";

// Navigate without a full page reload after hydration.
export default function HomePage() {
  return <Link href="/posts/42">Read the first post</Link>;
}

Same-origin links prefetch on intent (hovering over or pressing a link) by default. Setprefetch={false} to disable this.

Use the replace prop, such as <Link href="/dashboard" replace>Dashboard</Link>, to replace the current browser history entry instead of adding a new one, for example when leaving a login page.

NavLink adds active state for menus. Usenavigate("/hello?name=Nina") when navigation follows an event that has no link. All three are imported from goribu.

Metadata

Each page can set its own metadata through a meta export. Shared document tags, such as the viewport and favicon, belong in _document. Page metadata can be a plain object:

Static metadata
export const meta = {
  title: "About",
  description: "How our team builds on Cloudflare.",
  canonical: "https://example.com/about",
  robots: "index,follow",
};

export default function AboutPage() {
  return <main>About us</main>;
}

Metadata can also be a synchronous function receiving the same props as the page and its URL. For a post route, like the one earlier, add:

Dynamic metadata
export function meta({ props: { post } }) {
  return {
    title: post.title,
    description: `Read ${post.title} on our blog.`,
  };
}

Styling

The starter has Tailwind configured by default so utility classes work immediately. Plain CSS works too. Just edit src/styles.css or import CSS from a page or component. Goribu delivers the styles for the current page, including on client navigation.

Tailwind belongs to the app’s Vite configuration; plain CSS does not require a different Goribu API.

Middleware

Middleware runs on the server before a matched route's handler or page. Default-export a function from _middleware.js to apply it to routes in that folder and its subfolders. For example, this adds a request ID header to responses from the posts routes:

src/routes/posts/_middleware.js
export default function requestId(req, res) {
  res.headers({ "x-request-id": crypto.randomUUID() });
}

Middleware receives the same req and res as the route and can be async. Return nothing to continue (as above) or return ares.* result such as res.redirect("/login") to respond immediately and stop the execution chain from running. There is no next().

When several middleware files apply, parent folders run first. A_middleware.js directly in src/routes/ applies to every route. Middleware keeps affected pages running on the Worker, so put it in the folder that needs it.

Cookies

Use req.cookies to read, set and delete cookies in handlers or middleware. Goribu adds cookie changes to the returned response automatically, including redirects. This route keeps a counter in the browser's cookies:

src/routes/visits.js
export function GET(req, res) {
  const visits = Number(req.cookies.get("visits") ?? "0") + 1;
  req.cookies.set("visits", String(visits), {
    path: "/",
    maxAge: 86400,
    httpOnly: true,
    sameSite: "Lax",
  });
  return res.json({ visits });
}

Visit /visits and reload to see the counter increase. get()returns a string or null if the cookie is missing. Cookie options are explicit. Here path: "/" makes the cookie available across the app andmaxAge keeps it for a day.

To remove it, call req.cookies.delete("visits", { path: "/" })from a handler or middleware. Use the same path and domain, if specified, that you used when setting it.

Server code and errors

Page components run on the server and in the browser. Keep private helpers in files such as src/lib/posts.server.js outside src/routes/ and import them from handlers or middleware. Goribu rejects browser code that imports a.server.* module. Anything passed through res.render() reaches the browser too, so pass only the data the page should expose.

Use req.waitUntil(promise) in a handler or middleware for background work that may continue after the response, such as logging or updating a cache. Await work that must finish before you return a successful response.

Return res.notFound() for a missing resource. Customizesrc/routes/_404.jsx for missing pages and _500.jsx for unhandled production errors. Development shows the error details; production logs them with an error ID and passes that ID to the error page.

Environment variables

Put local values in a .env file at the project root:

.env
PUBLIC_API_URL=https://api.example.com
STRIPE_KEY=local-test-key

Read server values in handlers or middleware through req.env.STRIPE_KEY. Read browser values through import.meta.env.PUBLIC_API_URL. Anything prefixed PUBLIC_ is embedded in the browser bundle at build time and must never contain secrets.

Use .env.local to override values from .env during local development.

Put production values in .env.production. pnpm run deployuploads its keys as Worker secrets and preserves existing secrets missing from the file. Without that file, it uses the secrets already on the Worker. Local .envvalues never serve as a fallback for production Worker secrets. pnpm checkshows which option the deployment will use.

Browser values follow Vite's mode files: .env supplies shared values,.env.development supplies development values, and.env.production overrides the shared values for production builds..env.development does not supply server bindings. PUBLIC_*values stay public even when also uploaded as Worker secrets.

Cloudflare resources configuration

Declare a D1 database, a KV namespace, or both in the existing config. Keep your app's name and compatibility date:

goribu.config.js
import { defineConfig } from "goribu/server";

export default defineConfig({
  name: "my-app",
  compatibilityDate: "2026-08-31",
  database: { type: "d1", name: "my-app-db" },
  kv: { name: "my-app-kv" },
});

Restart development after changing bindings. Goribu supplies local stores for development and creates or reuses the named production resources on deployment. Local data is separate from production. Goribu generates the Wrangler bindings; you do not need resource IDs or a separate Wrangler config.

D1 database

D1 is Cloudflare's SQLite database. With database configured above, you can use req.d1 in handlers or middleware. Here we insert a post, read it back, then read all posts:

Inside a request handler
const title = "Hello Goribu";

const { lastRowId } = await req.d1`
  INSERT INTO posts (title) VALUES (${title})
`.run();

const post = await req.d1`
  SELECT id, title FROM posts WHERE id = ${lastRowId}
`.get();

const posts = await req.d1`SELECT id, title FROM posts`.all();

Values inside ${...} become bound SQL parameters, so you can pass user input without building SQL strings yourself. .run() executes inserts, updates, and deletes, returning { changes, lastRowId }..get() returns one row or null, .all() returns an array of rows, and .value() returns the first column of the first row ornull.

To create the example's table, run pnpm exec goribu migrate:new create posts. Add CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL); under -- migrate:up in the generated file, then runpnpm exec goribu migrate to apply it locally. Keep applied migrations unchanged and create new ones for later schema changes.

KV store

KV is Cloudflare's key-value store, useful for caching public data. With kvconfigured as above, use req.kv in handlers or middleware to read and write values. There are no tables or migrations. For example this route caches a timestamp for five minutes:

src/routes/snapshot.js
export async function GET(req, res) {
  let snapshot = await req.kv.get("snapshot", { type: "json" });
  if (snapshot === null) {
    snapshot = { createdAt: new Date().toISOString() };
    await req.kv.put("snapshot", JSON.stringify(snapshot), { expirationTtl: 300 });
  }
  return res.json(snapshot);
}

Visit /snapshot to try it. get() returns text ornull by default; { type: "json" } parses a stored JSON value. Use JSON.stringify() when writing objects, andexpirationTtl to set their lifetime in seconds.

Use await req.kv.delete("snapshot") to remove a key. await req.kv.list() returns a page of keys with pagination information. Await writes and deletes before reporting success.

KV is eventually consistent. After a successful write or delete, reads can still return an older value or null. Use it when temporary staleness is acceptable. Use D1 for exact counters or flows that need to read back a visitor's own changes reliably.

Going live

Set your app's Worker name in goribu.config.js. With a Cloudflare account, log in once from the app directory with Wrangler:

Terminal
pnpm --dir node_modules/goribu exec wrangler login

Then preview the production plan and deploy:

Terminal
pnpm check
pnpm run deploy

check shows the target Worker, resources, and migrations without changing them. deploy builds the app, creates or reuses the declared D1/KV resources, applies pending production migrations, and uploads the Worker and static assets. It prints your public URL.

Run pnpm run deploy again to publish changes to the same Worker. Existing databases and KV namespaces are reused; local data is not uploaded. Migrations run before the new Worker is uploaded, so schema changes must also work with the version currently serving requests.