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:
- Exact static paths --
/aboutbeats/[slug] - Longer prefixes --
/blog/postsbeats/blog - Static segments over dynamic --
/blog/featuredbeats/blog/[slug] - Dynamic segments over catch-all --
/docs/[section]beats/docs/[...slug] - 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.
When you add a new HTTP method handler (e.g. export async function DELETE) to an existing route.ts that previously only exported GET, the masquerade-side route table must be explicitly purged. Until you do, the new verb returns 405 Method Not Allowed. Run curl -X POST http://127.0.0.1:8444/nginx-cache/purge-site -H "Content-Type: application/json" -d '{"host":"<your-host>"}' after adding the method.
Related#
- PRISM Rendering Pipeline — how file-based routes compile into V8 bundles
- Caching — per-route ISR, TTL, and SWR settings that complement route rules
- Frameworks: Routing — framework-specific routing conventions (Next.js app router, Hono, etc.)
- i18n — locale prefix routing,
Content-Language, and per-locale cache keys - Configuration: bext.config.toml — full
[[route_rules]]reference