Routing

bext uses a high-performance trie-based router for O(depth) route matching. Routes are discovered automatically from your project's file structure and can be augmented with rewrites, redirects, and per-route rendering rules in bext.config.toml.

File-Based Routing

Place page files inside your app directory (default: src/app/) and bext maps the directory structure to URL paths. The scanner recognizes page.tsx, page.ts, page.jsx, and page.js as page components.

src/app/
  page.tsx             ->  /
  about/page.tsx       ->  /about
  blog/page.tsx        ->  /blog
  blog/[slug]/page.tsx ->  /blog/:slug
  contact/page.tsx     ->  /contact

Layout files (layout.tsx) are automatically detected and nested. Error boundaries (error.tsx), loading states (loading.tsx), and not-found pages (not-found.tsx) are co-located with their routes and picked up by the scanner.

You can change the app directory with server.app_dir:

[server]
app_dir = "src/pages"

Dynamic Routes

Use bracket syntax for dynamic path segments. The parameter value is available to your page component at render time.

File Path URL Pattern Example Match
blog/[slug]/page.tsx /blog/:slug /blog/hello-world
users/[id]/posts/[postId]/page.tsx /users/:id/posts/:postId /users/42/posts/7

Dynamic segments are captured as named parameters. Multiple dynamic segments in a single route are fully supported.

Catch-All Routes

Use the spread syntax [...slug] to match any number of path segments:

src/app/
  docs/[...slug]/page.tsx  ->  /docs/*slug

This matches /docs/getting-started, /docs/guides/routing, /docs/a/b/c, and so on. The slug parameter receives the full remaining path as a string (e.g., "guides/routing").

API Routes

Files named route.ts, route.js, or route.tsx are treated as API route handlers instead of pages. Export named functions for each HTTP method:

src/app/
  api/users/route.ts       ->  /api/users (GET, POST)
  api/users/[id]/route.ts  ->  /api/users/:id (GET, PUT, DELETE)

The scanner inspects exports and registers only the methods your file provides (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS).

Rewrites and Redirects

Configure URL rewrites and redirects in bext.config.toml without touching application code. These are processed before the route trie is consulted.

# Rewrites (internal, URL stays the same in the browser)
[[route_rules]]
pattern = "/old-blog/*"
proxy = "blog-upstream"

# Redirects (301 by default)
[[route_rules]]
pattern = "/legacy/*"
headers = { "location" = "/new/$1" }

# Per-route rendering overrides
[[route_rules]]
pattern = "/blog/*"
render = "isr"
ttl_ms = 3600000
swr_ms = 600000

[[route_rules]]
pattern = "/dashboard/*"
render = "ssr"
cache = false

[[route_rules]]
pattern = "/marketing/*"
render = "static"

The render field accepts four modes:

Mode Behavior
ssr Server-side render on every request, no caching
isr Incremental static regeneration with TTL and stale-while-revalidate
static Pre-rendered at build time, served from disk
swr Always serve cached, regenerate in background

Route Priority

When multiple patterns could match a request, bext resolves ambiguity with a fixed priority order:

1. Exact static paths -- /about beats /[slug] 2. Longer prefixes -- /blog/posts beats /blog 3. Static segments over dynamic -- /blog/featured beats /blog/[slug] 4. Dynamic segments over catch-all -- /docs/[section] beats /docs/[...slug] 5. Earlier definition order -- when two patterns are structurally identical, the one defined first in the file tree wins

Route rules defined in bext.config.toml follow longest-prefix-wins ordering. A rule with pattern /products/:id overrides a more general /products/* rule for paths like /products/42.

i18n Locale Routing

When i18n is configured, the router extracts a locale prefix from the URL before matching:

[i18n]
locales = ["en", "fr", "de", "es"]
default_locale = "en"
strategy = "prefix"

With prefix strategy, /fr/about resolves to locale "fr" and route /about. Requests without a locale prefix are redirected to /{default_locale}{path}. The locale is included in ISR cache keys so each language version is cached independently, and the Content-Language header is set automatically.

Custom Headers per Route

Route rules can inject response headers on matched paths:

[[route_rules]]
pattern = "/api/*"
headers = { "cache-control" = "no-store", "x-robots-tag" = "noindex" }

[[route_rules]]
pattern = "/assets/*"
headers = { "cache-control" = "public, max-age=31536000, immutable" }

This is useful for setting security headers, cache policies, or CORS overrides on specific route groups without modifying application code.