Start
The Mental Model
A Goribu app is one Worker that serves two things from the same place: server-rendered HTML pages and the JSON a single-page app needs to update them. The whole framework follows from how those two come out of one handler. Hold on to this one idea and the rest of the docs stop being a list of features and become consequences of it.
Here is a complete route. A handler loads data and calls res.render while the default export is the page it renders.
// src/routes/posts/[id].jsx
export async function GET(req, res) {
const post = await req.d1.get("SELECT * FROM posts WHERE id = ?", [req.params.id]);
if (!post) return res.notFound();
return res.render(PostPage, { post });
}
export default function PostPage({ post }) {
return <article><h1>{post.title}</h1><p>{post.body}</p></article>;
}One handler, two responses
Follow a request through it.
On a first visit someone types the URL or follows a link from outside, the handler runs, res.render renders PostPage with its props, wraps it in your Document and streams HTML to the browser. The page is visible immediately, then the client code loads and hydrates it into a live React tree.
On an in-app navigation, when someone already on the site clicks a <Link>, the same handler runs again, but this time res.render returns a small JSON payload: the component to show, its props and the page metadata. The client receives that, swaps the page in place and updates the URL, with no full reload.
You write a single handler. res.render looks at the request and decides which representation to send. That is the base of the framework: the page route is also its own data source, so there is no separate API layer to build. You do not stand up /api/posts/:id to feed a client that is already asking for /posts/:id. The route already answers in both formats.
What follows from it
Because a page is produced by handing props to a component, a few things hold everywhere in Goribu, and they are worth recognizing as the same idea rather than separate rules:
- Pages are pure functions of their props. The handler is where data, branching and decisions live; the component only renders what it is given. This is why showing a validation error is just re-rendering the same component with an
errorsprop — and why everyres.rendermust pass the complete set of props the page needs, not a partial update. - The server is the source of truth. Client state is for things that are genuinely client-only — a menu that is open, an optimistic toggle awaiting confirmation — not for your application's data. The data came from the server and the server can render it again.
- Forms are the same shape as navigation. A
<Form>submits to a handler; if the handler renders or redirects, the page navigates, and if it returns JSON the page stays put and reacts in place. Same handler model, pointed at a mutation. - Prerendering is the model run early. A handler that reads nothing request-specific produces the same HTML every time, so Goribu can run it once at build time and serve the result from the CDN — the Worker never wakes up. Read a piece of request data and the route opts back into running per request. Nothing special to configure; it is the same render, just cached when it safely can be.
That is the mental model in full. From here, the Reference section documents each surface — Routes, Req & Res, Rendering, Forms — but every one of them is this same loop seen up close.