Routing
File-Based Routing
Map files to URLs for pages, APIs, markdown routes, dynamic params, catchalls, and route groups.
Routing is driven by files under app/routes. The shape is close to Next-style conventions, but it stays Bun-native and keeps page and API routes in one tree.
Minimal working example
app/routes/
index.tsx
tasks/
index.tsx
[id].tsx
[...filters].tsx
api/
tasks.ts
docs/
start/
overview.md
This resolves to:
//tasks/tasks/:id/tasks/*filters/api/tasks/docs/start/overview
Dynamic params
import { useParams } from "react-bun-ssr/route";
export default function TaskRoute() {
const params = useParams();
return <h1>Task {params.id}</h1>;
}
Markdown routes
A .md file under app/routes is a first-class page route. The framework compiles it into a generated wrapper module so SSR and hydration use the same output.
Colocated private files
Files whose basename starts with _ are ignored by route scanning, except for the reserved _layout and _middleware conventions.
That gives you a simple way to colocate page-only helpers next to a route without creating accidental public URLs.
app/routes/
tasks/
index.tsx
_card.tsx
_format-date.ts
_header.module.css
In that example:
tasks/index.tsxcreates/tasks_card.tsxdoes not create a route_format-date.tsdoes not create a route_header.module.cssstays a normal colocated stylesheet
Important: this rule applies to files, not folders. A folder like _admin/ still behaves like a normal route segment and would map into the URL unless you use a route group like (admin).
404 handling
Routing also owns the not-found path. There are two main cases:
- no route matches the URL at all
- a dynamic route matches, but the requested resource does not exist
For the first case, export NotFound from app/root.tsx or a layout route to render the nearest 404 UI.
For the second case, throw notFound() from a loader after the route has matched:
import { notFound, type Loader } from "react-bun-ssr/route";
export const loader: Loader = async ({ params }) => {
const task = await loadTask(params.id ?? "");
if (!task) {
throw notFound({ entity: "task", id: params.id });
}
return { task };
};
That keeps URL matching and missing-resource handling separate, which is the right model for dynamic routes.
Customize the NotFound page
Export NotFound from the route, layout, or root module where you want the 404 UI to be defined.
// app/root.tsx
import { Link } from "react-bun-ssr/route";
export function NotFound() {
return (
<main>
<h1>Page not found</h1>
<p>The page you requested does not exist or is no longer available.</p>
<p>
<Link to="/docs">Back to the docs</Link>
</p>
</main>
);
}
You can scope the 404 UI at different levels:
- export
NotFoundfromapp/root.tsxfor a site-wide default 404 page - export
NotFoundfrom a_layout.tsxto customize 404 behavior for a section - export
NotFoundfrom a matched page route when missing data should render a route-specific not-found state
Resolution order is:
- matched route
NotFound - nearest layout
NotFound - root
NotFound
Rules
.mdis supported as a page route..mdxroute files are rejected explicitly._layoutand_middlewareparticipate in routing but do not become public URLs.- Other files that start with
_are treated as private colocated files and do not become routes. - Folders that start with
_still behave like normal route segments. - Route-group directories like
(marketing)affect organization, not the URL. - Use
NotFoundfor unmatched URLs andnotFound()for matched routes with missing data. - The nearest
NotFoundexport wins.
Related APIs
Next step
Use Layouts and Groups to shape the route tree, then read Middleware for request-pipeline behavior.