bext.dev
DocsGuides
Guides4 min read
On this page

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.

code
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:

toml
[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:

code
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:

code
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.

toml
# 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:

toml
[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:

toml
[[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.

Warning

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.

Edit this page ↗Need a hand? ↗
FIND YOUR NEXT STEP

Start with a topic, a command, or a question.