DEV Community

Cover image for One regex to match them all
Vadim Ferderer
Vadim Ferderer

Posted on

One regex to match them all

Last year I was building a single-page app with SvelteKit. Nothing fancy—a handful of views, client-side navigation, and zero need for server-side rendering. Should be simple, right?

It wasn't. SvelteKit assumes you want SSR. It assumes you want its file-based routing. It assumes you want its data-loading conventions. I didn't want any of that. I just wanted a URL to map to a component.

So I started looking at alternatives.

Router Size (min+gz) Framework-agnostic? Nested routes? Single-regex matching?
TanStack Router ~44 kB No (React) Yes No
Vue Router ~28 kB No (Vue) Yes No
navigo ~9 kB Yes No No
wouter ~3.5 kB No (React) Limited No

I looked at this and thought, why does URL matching require 44 kB?

The single regex trick

Here's the core insight behind Texivia: most routers store routes in an array or a tree and iterate through them on every navigation. You have 30 routes? That's up to 30 pattern matches per URL change. It's O(n).

Texivia takes a different approach. When you define your routes, it compiles all of them into a single combined regex with positional groups:

/^(?:\/)|(?:\/users\/(\d+))|(?:\/about)$/
Enter fullscreen mode Exit fullscreen mode

One call to regex.exec(url). The position of the matched group tells you which route was hit. Named capture groups like (?<id>...) are only used internally for parameter extraction—the route identification itself is purely positional. Zero iteration. I originally invented this technique years ago for a PHP router I built for a telecom project. The idea is simple: regex engines are among the most optimized pieces of code in any language runtime. Why write your own matching loop when you can hand the entire problem to the engine in a single call?

The fastest code is the code you never execute.

The API

import { Router } from 'texivia-router'

import { Home }         from './pages/Home.js'
import { Counter }      from './pages/Counter.js'
import { RecipeDetail } from './pages/RecipeDetail.js'
import { NotFound }     from './pages/NotFound.js'

export const router = new Router([
  { path: '/',              view: Home         },
  { path: '/counter',       view: Counter      },
  { path: '/recipe/{id}',   view: RecipeDetail },
  { path: '*',              view: NotFound     },
])
router.start()
Enter fullscreen mode Exit fullscreen mode

Routes are plain objects, defined in TS like

type ConfigRoute<T> = {
  path: string;
  view?: T;
  redirect?: string;
  handler?: HookType<T>;
}
Enter fullscreen mode Exit fullscreen mode

Dynamic segments use {paramName} syntax with optional regex constraints like {id:\\d+}.

Navigation is programmatic or event-driven:

router.navigate('/about');
Enter fullscreen mode Exit fullscreen mode

Every navigation fires a native CustomEvent:

document.addEventListener('texivia', (event) => {
  const { route, params } = event.detail;
  // render your view however you want
});
Enter fullscreen mode Exit fullscreen mode

No virtual DOM. No framework lifecycle hooks. The browser already has an event system—Texivia just uses it.

What I deliberately left out

Every feature I didn't add was a conscious decision.

No lazy loading API. await import('./HeavyView.js') already exists at the language level. Wrapping it adds abstraction for the sake of API surface.

No data fetching. Your routing layer should not own your data layer. Mixing them creates coupling that makes migrations painful three years down the line.

No state management. A router knows about URLs. It shouldn't know about your application state.

No framework dependency. The texivia event carries all the information you need. Wire it to React, Svelte, Vue, lit-html, or document.getElementById — the router doesn't care.

The philosophy

I've spent 25+ years in enterprise software—mostly C++, Java, Spring Boot, and performance engineering. One project I inherited was running for 225 minutes. I got it down to 90 seconds. The approach is always the same: find out what the code is doing that it doesn't need to do, and stop doing it.

The frontend ecosystem has been doing the opposite for over a decade. Every framework wants to own more of your stack. Your router fetches your data. Your meta-framework decides between SSR and CSR on your behalf.

Texivia matches a URL to a route name in a single regex call, fires a CustomEvent, and gets out of your way. ~1.2 kB. Nothing left to remove.

npm: npm install texivia-router
GitHub: https://github.com/ferderer/texivia

Top comments (0)