Project structure

The folders and files in a Bini.js project, and how they turn into routes.

Overview

Everything the router reads lives in src/app. Add a file there and you get a page. Add a file to src/app/api and you get an endpoint. There is no router config to write.

Default project layout

This is the src folder created by npx create-bini-app@latest.

src
app
api
ƒhello.ts
layout.tsx
page.tsx
globals.css
main.tsx
App.tsx
PathWhat it does
src/appYour routes and layouts. The router scans this folder.
src/app/apiAPI handlers. hello.ts is served at /api/hello.
src/app/layout.tsxRoot layout. Wraps every page and holds global metadata.
src/app/page.tsxThe home page, served at /.
src/app/globals.cssGlobal styles imported by the root layout.
src/main.tsxApp entry point. Also exports render() used for static site generation.
src/App.tsxRoute tree generated by bini-router. It is rewritten when routes change, so do not edit it.
publicStatic assets served from the site root, such as favicon.ico and site.webmanifest.

Top-level files

Files in the project root configure tooling. None of them affect routing.

FilePurpose
vite.config.tsVite configuration. Registers bini-router, bini-env and the other plugins.
package.jsonDependencies and scripts such as dev, build and deploy.
.envEnvironment variables. BINI_* is exposed to the browser, everything else stays on the server.
.oxlintrc.jsonOxlint configuration.
.oxfmtrc.jsonOxfmt configuration.

Routing files

Some file names have a fixed job inside src/app. A routing file applies to its own folder and every folder below it. A file in a subfolder overrides the same file from a parent, and if none exists Bini.js falls back to a built-in default.

layout.tsx
template.tsx
error.tsx
loading.tsx
not-found.tsx
page.tsx
Component hierarchy
<Layout>
  <ErrorBoundary fallback={<Error />}>
    <Suspense fallback={<Loading />}>
      <Template>
        <Page />
      </Template>
    </Suspense>
  </ErrorBoundary>
</Layout>

These files wrap a page in a fixed order. The layout is outermost. Inside it, an error boundary catches errors and shows error.tsx, and a Suspense boundary shows loading.tsx while the page loads. The template wraps the page itself, and the page is the innermost piece. not-found.tsx is not part of this wrapper. It renders when no route matches, inside the same layout chain.

FilePurpose
page.tsxThe UI for a route. Can also be .mdx or .md.
layout.tsxShared UI for a folder and its children. Renders children through <Outlet />.
template.tsxLike a layout, but wraps each page individually.
loading.tsxFallback shown while a page or layout loads.
error.tsxFallback shown when something in the folder throws.
not-found.tsxUI for URLs that match no route under the folder.
default.tsxFallback for a parallel route slot with no match.
Only page files can be MDX or Markdown. Every other routing file must be .tsx, .jsx, .ts or .js.

Folders and files become routes

Folders define URL segments, and nesting folders nests segments. A file becomes a route in two ways: a page file inside a folder, or a flat file placed directly in a folder. A file named index is treated the same as page and maps to its parent folder.

app
page.tsx
about.tsx
blog
page.tsx
[slug].tsx
dashboard
layout.tsx
page.tsx
/Routable
/aboutRoutable
/blogRoutable
/blog/:slugRoutable
/dashboardRoutable

A page.tsx in the root is the home page. A flat file such as about.tsx becomes /about without needing its own folder. Inside blog, page.tsx maps to /blog, and [slug].tsx is a dynamic route where :slug matches any value. A layout.tsx has no URL of its own. It wraps its folder's routes and everything below them.

Colocation and private folders

You can keep components, helpers and data files next to the routes that use them. Anything starting with _ or . is skipped by the router, along with everything inside it. Prefix a folder with an underscore, such as _components, to mark it as a private implementation detail.

app
_components
button.tsx
_lib
constants.ts
dashboard
page.tsx
_nav.tsx
api
ƒhello.ts
_db.ts
/_components/buttonNot Routable
/_lib/constantsNot Routable
/dashboardRoutable
/dashboard/_navNot Routable
/api/helloRoutable
/api/_dbNot Routable

Files inside a private folder, like button.tsx and constants.ts, never produce a URL. The same goes for files with an underscore prefix, like _nav.tsx and _db.ts. The routable files next to them, page.tsx and hello.ts, keep working as normal, so a route can hold its own helpers without exposing them.

Route groups

Wrap a folder name in parentheses, such as (marketing), to organize routes without changing their URLs. The group name is dropped from the path, and the group can have its own layout, loading and error files.

app
(marketing)
layout.tsx
about
page.tsx
(shop)
layout.tsx
cart
page.tsx
/aboutRoutable
/cartRoutable

Because the group name is dropped, the pages get plain URLs: /about and /cart. Each group's layout.tsx only wraps the routes inside that group, so marketing pages and shop pages can share one URL space while looking completely different.

Organizing your project

Bini.js does not enforce where non-route code lives. Pick one approach and stay consistent across the project. The folder names below, components and lib, are placeholders with no special meaning.

Store shared code outside src/app

Keep all shared code in folders next to app and use src/app only for routing. Nothing here can become a route by accident.

src
components
lib
app
page.tsx

Split code by route with private folders

Keep code that only one route uses inside that route's folder, in _ folders. Shared code can stay in the folders from the first approach.

checkout
page.tsx
_components
summary.tsx
_lib
totals.ts
Was this helpful?