Reference
Routes
Routes are just files in src/routes/. The path on disk is the URL and a single file holds both the request handlers for that URL and the React page they render. There is no pages/ and api/ split. And no route table to keep in sync. You add a file, you have a route.
A route file's default export is the page. Its named exports, GET, POST and the other HTTP methods, are the handlers. The handler runs first, reads what it needs, then hands data to the page with res.render:
// 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(Post, { post });
}
export default function Post({ post }) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}That's the whole model. A file, one or more handlers and a page.
The routes folder
The filename becomes the URL. index is the root of its folder. Folders nest.
src/routes/
├── index.jsx → /
├── about.jsx → /about
├── blog/
│ ├── index.jsx → /blog
│ └── archive.jsx → /blog/archive
└── settings/
└── account.jsx → /settings/accountThere is nothing special about any folder name. src/routes/api/users.jsx serves /api/users because that is what the path says, not because Goribu treats api differently. Group your files however you prefer.
Dynamic segments
Wrap a segment in square brackets to make it dynamic. It matches any single value in that position.
src/routes/
├── posts/
│ └── [id].jsx → /posts/:id
└── [category]/
└── [slug].jsx → /:category/:slugThe matched values arrive on req.params, already URL-decoded:
export function GET(req, res) {
const { id } = req.params;
// ...
}A dynamic segment matches exactly one segment. It never matches an empty value or spans a slash. There are no catch-all ([...rest]) or optional segments; a route matches only when its segment count matches the URL's.
Pages, handlers, or both
A route file can be a page, an endpoint or both. The named handler exports decide which methods the route answers. The default export decides whether there is a page to render.
Co-locating a page with the action that posts to it is the common case. Like the form and the code that handles it live in one file:
// src/routes/login.jsx
import { Form } from "goribu";
export function GET(req, res) {
return res.render(Login);
}
export async function POST(req, res) {
// ...handle the submission...
return res.redirect("/dashboard");
}
export default function Login() {
return (
<Form method="post">
<input name="email" type="email" />
<input name="password" type="password" />
<button>Sign in</button>
</Form>
);
}A page needs a GET handler that calls res.render to be served. There is no implicit render of the default export. See Request and Response for everything req and res expose and Forms for <Form>.
API-only routes
A route file with no default export is just an HTTP endpoint. Same handler shape, no page.
// src/routes/health.jsx
export function GET(req, res) {
return res.json({ ok: true });
}Endpoints can live anywhere under src/routes/. Many apps gather JSON endpoints under src/routes/api/. Goribu does not require it, but it works fine.
Going deeper
Route matching and precedence
When more than one route could match a URL, the more specific one wins: a static segment beats a dynamic segment in the same position. So /posts/new is served by posts/new.jsx, not posts/[id].jsx, even though both could match.
Two files that map to the same URL shape are a build error. Dynamic parameter names do not make routes distinct, so [id].jsx and [slug].jsx in the same folder conflict. Rename or remove one.
Automatic method behavior
You export only the methods you care about; Goribu fills in the rest.
HEADis derived fromGET. AHEADrequest runs yourGEThandler and returns the response with the body removed. Export your ownHEADto override.OPTIONSis answered with204 No Contentand anAllowheader listing the methods the route supports. Export your ownOPTIONSto override.- An unsupported method returns
405 Method Not Allowedwith the sameAllowheader.
Method names must be uppercase: GET, POST, PUT, PATCH, DELETE.
Which files become routes
A route is any .js, .jsx, .ts or .tsx file whose name does not start with _. Underscore-prefixed files are reserved for framework features, _middleware, _404, _500, and never become URLs. TypeScript declaration files (.d.ts) are skipped too. index is stripped only as a filename; a folder literally named index is a normal URL segment.
A route with no default export that calls res.render is a build error: with no page to hydrate, the render would ship dead HTML. Use res.html for ad-hoc HTML or res.notFound for 404s, or add the missing export default.
Unmatched URLs
A URL with no matching route returns a 404. Customize it with src/routes/_404.jsx. See Error Pages.
Static and prerendered routes
A route that needs no request-time data can be served as a static file straight from Cloudflare's CDN, so the Worker never runs for it. Dynamic routes can opt in by exporting prerender with the parameter values to bake at build time. Unlisted values still hit the GET handler at runtime. See Rendering for the full rules.