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.