Skip to main content

Guide

radix3 vs find-my-way vs trek-router 2026

Compare radix3, find-my-way, and trek-router for Node.js HTTP routing in 2026. Radix tree performance, constraints, wildcard matching, which router to pick.

·PkgPulse Team·
0

TL;DR

radix3 is the UnJS radix tree router — ultra-fast path matching, used internally by H3 and Nitro, supports static routes, parameters, and wildcards with ~5M weekly downloads. find-my-way is Fastify's router — radix tree with HTTP method support, parametric routes, constraint-based routing (version and host), and consistently the fastest benchmarked Node.js router at ~5M weekly downloads. trek-router is a lightweight trie-based router — simple API, named parameters, around ~50K weekly downloads, primarily for educational use. For production: find-my-way if you need a standalone router, radix3 if you're in the UnJS ecosystem, trek-router only for learning or minimal prototypes.

Quick Comparison

radix3find-my-waytrek-router
Weekly Downloads~5M~5M~50K
Data StructureRadix treeRadix treeTrie
HTTP Methods❌ (path only)
Named Parameters
Wildcard Routes
Constraints✅ (version, host)
TypeScript⚠️
Bundle Size~3KB~15KB~2KB
Used ByH3, Nitro, NuxtFastify

Quick Comparison

radix3find-my-waytrek-router
Weekly Downloads~5M~5M~50K
GitHub Stars~1.4K~4.5K~700
Bundle Size~2 KB~15 KB~2 KB
TypeScript⚠️ partial
LicenseMITMITMIT
HTTP Methods❌ path-only
Constraints
MaintenanceActiveActiveMinimal

How Router Engines Work

Most developers interact with routers through a framework like Express, Fastify, or Hono — but understanding the underlying routing engine helps you pick the right framework and debug performance issues. Router engines solve a specific problem: given an incoming URL path (and optionally an HTTP method), find the matching handler as fast as possible.

The naive approach is a linear scan: store routes in an array and test each pattern against the incoming URL. This is simple to implement but becomes O(n) per request — with 100 routes, you check up to 100 patterns. At high traffic, this adds up fast.

Radix trees (also called Patricia tries or compressed prefix trees) solve this by organizing routes into a tree structure where shared prefixes are merged. A path like /api/packages/:name shares the prefix /api/packages/ with /api/packages/:name/versions/:version. Instead of checking both patterns independently, the router traverses the tree once. Lookup becomes O(path length) regardless of how many routes are registered — 1,000 routes are as fast to query as 10 routes.

This is why high-performance Node.js frameworks use radix tree routers. The difference between O(n) and O(path length) becomes significant at scale: a server handling 50,000 requests per second with 200 routes would spend meaningfully less CPU time on routing alone with a radix tree vs. a linear scan.

Linear scan (naive):
  routes = ["/api/users", "/api/users/:id", "/api/posts", ...]
  For each request, scan ALL routes until match → O(n) per request

Radix tree (fast):
  /api
    /users
      /:id    → handler
      /        → handler
    /posts
      /:id    → handler
      /        → handler

  Lookup is O(path length), not O(route count)
  1000 routes? Same speed as 10 routes.

radix3

radix3 is the UnJS organization's standalone radix tree router. It was extracted from the H3 HTTP framework to be reusable across the UnJS ecosystem. The package does exactly one thing: match URL paths against a registered set of patterns, with support for static routes, named parameters (:name), and wildcards (**). There is no HTTP method matching — radix3 operates at the path level only, leaving method routing to the framework layer above it.

This design choice makes radix3 very focused and lightweight (~3KB). If you're building on top of H3, Nitro, or Nuxt, you get radix3 routing automatically — your Nuxt server routes and Nitro API handlers all compile to radix3 lookups under the hood. The router handles the URL-to-handler mapping while the framework handles everything else: body parsing, response serialization, middleware execution, and lifecycle hooks.

The API is minimal: createRouter(), insert(), lookup(), and remove(). You insert routes with arbitrary data objects attached (typically a handler function reference, but it can be anything), and lookup returns that data plus any extracted parameters.

Basic Usage

import { createRouter } from "radix3"

const router = createRouter()

// Static routes:
router.insert("/api/health", { handler: "healthCheck" })
router.insert("/api/packages", { handler: "listPackages" })

// Parameter routes:
router.insert("/api/packages/:name", { handler: "getPackage" })
router.insert("/api/packages/:name/versions/:version", { handler: "getVersion" })

// Wildcard:
router.insert("/static/**", { handler: "serveStatic" })

// Lookup:
const match = router.lookup("/api/packages/react")
// → { handler: "getPackage", params: { name: "react" } }

const match2 = router.lookup("/api/packages/react/versions/19.0.0")
// → { handler: "getVersion", params: { name: "react", version: "19.0.0" } }

const match3 = router.lookup("/static/images/logo.png")
// → { handler: "serveStatic", params: { _: "images/logo.png" } }

const miss = router.lookup("/not/found")
// → null

Route Management

Unlike some routers, radix3 supports removing routes at runtime, which enables hot-reloading scenarios in development frameworks.

import { createRouter } from "radix3"

const router = createRouter()

router.insert("/api/packages", { handler: "v1" })

// Remove:
router.remove("/api/packages")

// Re-insert with updated data:
router.insert("/api/packages", { handler: "v2" })

How H3 Uses radix3

When you write a Nuxt server route or a Nitro API handler, you're writing an H3 event handler that gets registered in a radix3 tree. The framework handles all the wiring — you just write the handler logic. This tight integration is why radix3 adoption follows H3/Nuxt/Nitro adoption closely.

// H3 (UnJS HTTP framework) uses radix3 internally:
import { createApp, createRouter, eventHandler } from "h3"

const app = createApp()
const router = createRouter()

// These routes are stored in a radix3 tree:
router.get("/api/packages", eventHandler(() => ({ packages: [] })))
router.get("/api/packages/:name", eventHandler((event) => {
  return { name: event.context.params.name }
}))

app.use(router)
// radix3 handles the path matching — O(path length) lookups

find-my-way

find-my-way is Fastify's core routing engine, published as a standalone package. It is consistently the fastest benchmarked Node.js router — outperforming koa-router by roughly 30x and Express's router by roughly 50x on static routes. Unlike radix3, find-my-way includes HTTP method support, constraint-based routing, and advanced parameter matching.

The constraint system is find-my-way's most distinctive feature. Beyond basic path matching, you can route based on the Accept-Version header (for API versioning) or the Host header (for multi-tenant or subdomain routing) — all within the same router instance without manually branching your code. This is how Fastify implements its built-in version routing feature.

Parameter handling is also more powerful than radix3's. find-my-way supports regex constraints on parameters (/:id(^\\d+$) to only match numeric IDs), multi-parametric segments (/:org-:repo matching facebook-react), and true wildcard capture.

Basic Usage

import FindMyWay from "find-my-way"

const router = FindMyWay()

// Routes with HTTP methods:
router.on("GET", "/api/packages", (req, res, params) => {
  res.end(JSON.stringify({ packages: [] }))
})

router.on("GET", "/api/packages/:name", (req, res, params) => {
  res.end(JSON.stringify({ name: params.name }))
})

router.on("POST", "/api/packages", (req, res, params) => {
  res.end(JSON.stringify({ created: true }))
})

// Wildcard:
router.on("GET", "/static/*", (req, res, params) => {
  res.end(`Serving: ${params["*"]}`)
})

// Lookup:
router.lookup(req, res)  // Finds and calls the matching handler

Parametric Routes and Constraints

const router = FindMyWay()

// Named parameters:
router.on("GET", "/api/packages/:name", handler)
// Matches: /api/packages/react → params.name = "react"

// Regex constraints on parameters:
router.on("GET", "/api/packages/:id(^\\d+$)", handler)
// Matches: /api/packages/123 (only numbers)
// Rejects: /api/packages/react

// Multi-parametric:
router.on("GET", "/api/:org-:repo", handler)
// Matches: /api/facebook-react → params = { org: "facebook", repo: "react" }

Version and Host Constraints

import FindMyWay from "find-my-way"

const router = FindMyWay()

// Version-based routing:
router.on("GET", "/api/packages", { constraints: { version: "1.0.0" } }, (req, res) => {
  res.end("API v1")
})

router.on("GET", "/api/packages", { constraints: { version: "2.0.0" } }, (req, res) => {
  res.end("API v2")
})

// Host-based routing:
router.on("GET", "/", { constraints: { host: "api.pkgpulse.com" } }, (req, res) => {
  res.end("API subdomain")
})

Performance Profile

find-my-way benchmarks (operations per second):

  Static routes:     ~15M ops/sec
  Parametric routes: ~5M ops/sec
  Wildcard routes:   ~8M ops/sec

  vs koa-router:     ~500K ops/sec (30x slower)
  vs express router:  ~300K ops/sec (50x slower)

  find-my-way is consistently the fastest Node.js router.

The performance gap comes from several implementation choices. find-my-way avoids dynamic property access and minimizes allocations in the hot path. Its radix tree implementation is more aggressively optimized than most alternatives, with specialized handling for common patterns like version sorting and constraint evaluation. These optimizations matter most under sustained high throughput, where routing overhead compounds into CPU time that could be spent on actual business logic.

How Fastify Uses find-my-way

// Fastify uses find-my-way internally for ALL route matching:
import Fastify from "fastify"

const app = Fastify()

// These routes are stored in find-my-way's radix tree:
app.get("/api/packages/:name", async (request) => {
  return { name: request.params.name }
})

// Fastify's constraint system (versioning) is powered by find-my-way:
app.route({
  method: "GET",
  url: "/api/data",
  constraints: { version: "2.0.0" },
  handler: async () => ({ version: 2 }),
})

trek-router

trek-router is a minimal trie-based router with a simple two-method API: add() and find(). It supports named parameters and HTTP methods but lacks wildcards, constraints, and route removal. Download volume (~50K/week) reflects its niche role — it's primarily used in educational contexts and minimal prototypes, not production applications.

Basic Usage

import Router from "trek-router"

const router = new Router()

// Add routes:
router.add("GET", "/api/packages", "listPackages")
router.add("GET", "/api/packages/:name", "getPackage")
router.add("POST", "/api/packages", "createPackage")

// Find:
const [handler, params] = router.find("GET", "/api/packages/react")
// handler → "getPackage"
// params → [{ name: "name", value: "react" }]

const [handler2, params2] = router.find("GET", "/not-found")
// handler2 → undefined
// params2 → undefined

trek-router Limitations

trek-router:
  ✅ Simple API (add + find)
  ✅ Trie-based (fast lookups)
  ✅ Lightweight (~2 KB)

  ❌ No wildcard routes
  ❌ No constraints
  ❌ No route removal
  ❌ Not actively maintained
  ❌ Limited parameter syntax

For production: use find-my-way or radix3

Route Precedence and Edge Cases

One important consideration when working with any radix tree router is route precedence — which route wins when multiple patterns could match a request. The standard rule across all three routers is: static routes beat parametric routes, which beat wildcard routes.

Route priorities (highest to lowest):
  1. Static: /api/packages/create
  2. Parametric: /api/packages/:name
  3. Wildcard: /api/**

For GET /api/packages/create:
  → matches /api/packages/create (static) — not /:name

This precedence is important when you want an endpoint like /api/packages/create to be treated as a static path, not captured as a parameter named name. All three routers implement this same precedence model, so switching between them doesn't change route matching semantics.

Understanding this prevents a common bug where developers register a static route expecting it to take priority, but get confused when their dynamic route also matches. The radix tree structure naturally handles this: static segments are checked before parametric segments at each tree node.

Choosing Your Router in Practice

The practical choice between these routers depends almost entirely on which framework you're using, not the router API itself. Very few developers interact directly with find-my-way or radix3 — they use them indirectly through Fastify and H3/Nuxt respectively. The router choice follows the framework choice.

If you're building a standalone HTTP routing library, need to embed a router in a custom runtime, or are building a new framework, find-my-way is the best default. It handles HTTP methods, supports constraints, has the best performance, and is the most battle-tested in production. Its use as Fastify's core router means it handles an enormous volume of real-world traffic.

radix3 is the right choice if you're deeply embedded in the UnJS ecosystem (Nitro, Nuxt, H3) and want to stay consistent with the ecosystem's internals. It's also useful when you want path matching without method routing — for example, matching URL patterns to static assets or configuration sections rather than HTTP handlers.

trek-router is not a production choice. Its maintenance status is unclear, it lacks features that real applications need (wildcards especially), and there's no reason to choose it over find-my-way or radix3 unless you're building a minimal learning project or proof-of-concept.

Feature Comparison

Featureradix3find-my-waytrek-router
Data structureRadix treeRadix treeTrie
HTTP methods❌ (path only)
Named parameters
Wildcard routes
Regex constraints
Version routing
Host routing
Route removal
TypeScript⚠️
Used byH3, NitroFastify
Weekly downloads~5M~5M~50K

When to Use Each

Use radix3 if:

  • Building with H3/Nitro (UnJS ecosystem)
  • Need a pure path matcher (no HTTP method binding)
  • Want the simplest possible radix tree router
  • Building custom routing logic on top

Use find-my-way if:

  • Building with Fastify or a standalone HTTP server
  • Need HTTP method support (GET/POST/PUT/DELETE)
  • Want constraint-based routing (versioning, host)
  • Need the fastest benchmarked router with the richest feature set

Use trek-router if:

  • Learning how trie/radix routers work
  • Need the most minimal router possible for a proof of concept

Why Router Performance Matters at Scale

Most Node.js applications will never need to think about router performance. Express's built-in router handles thousands of routes without measurable overhead at typical SaaS scale — a few hundred requests per second against a few dozen routes is nowhere near the limit. The performance characteristics of your database queries, serialization, and I/O will dominate response time by orders of magnitude compared to route lookup.

Router performance becomes genuinely critical in two scenarios. The first is very high-traffic APIs handling thousands of requests per second on a single process, where even microsecond-scale overhead per request adds up to meaningful latency at the tail. The second — and more practically relevant in 2026 — is serverless and edge function environments, where each invocation may pay a cold-start cost and bundle size directly affects initialization time. A radix tree router's O(path length) lookup characteristic means that adding 1,000 routes to your application doesn't slow down any individual route lookup at all. The lookup time is bounded by the length of the URL path, not the number of registered routes.

This is why Fastify consistently benchmarks three to four times faster than Express in synthetic tests: find-my-way's radix tree routing is one of several places Fastify gains performance across the stack. For the UnJS ecosystem — H3, Nitro, and Nuxt's server routes — radix3's lightweight design means the router itself contributes nearly nothing to edge function bundle sizes. When Nuxt deploys a server route to Cloudflare Workers, the router overhead is measured in bytes, not kilobytes.

For most applications the performance argument is academic. But if you're benchmarking a high-throughput service or optimizing a Cloudflare Worker that handles significant traffic, choosing the right router engine from the start avoids a later refactor. More practically, the choice of router usually dictates your framework choice and vice versa — they're coupled decisions, not independent ones.

Choosing a Router for Your Stack

The decision between these routers is almost always made for you by your framework choice, which is the right way to think about it. If you're building with Fastify, find-my-way is built in — you configure it through Fastify's options but you don't install or manage it separately. If you're building server routes for a Nuxt application, writing an H3 API, or using Nitro as a deployment target, radix3 is what powers the routing layer automatically. There is no decision to make.

The cases where you choose a standalone router explicitly are narrower: building your own micro-framework or HTTP middleware layer, adding route-matching logic to something that isn't a web server at all (matching CLI command patterns, file-system paths, or message routing), or writing a benchmarking harness to compare routing approaches. In these scenarios, find-my-way is the production choice for anything HTTP-related, and radix3 is the right pick for pure path-matching tasks where HTTP method semantics don't apply.

Trek-router's role in 2026 is primarily educational. Its simple add and find API makes trie-based routing easy to understand without the complexity of find-my-way's constraints system or radix3's wildcard handling. For a developer building their first routing layer from scratch, trek-router's source code is a useful reference. For any production service, its lack of wildcard support, inactive maintenance status, and limited parameter syntax make it a poor choice compared to either alternative. If you find yourself using trek-router in production, the migration path to find-my-way is straightforward — the conceptual model is the same and the API is similar enough that a find-and-replace with minor adjustments is usually sufficient.

HTTP Method Support: A Significant Architectural Difference

radix3 is intentionally a path-only router — it has no awareness of HTTP methods at all. This is a deliberate architectural decision from the UnJS team, reflecting their design philosophy that concerns should be separated cleanly: radix3 handles path matching, and H3 handles method dispatch at a higher level through its router.get(), router.post(), and similar methods. If you're using radix3 as a standalone router outside the H3 ecosystem, you are responsible for building HTTP method dispatch yourself.

find-my-way bakes HTTP method support into every route definition at the data structure level. When you call router.on("GET", "/path", handler) and router.on("POST", "/path", handler), these are stored separately in the radix tree with method-aware lookup. A request for GET /api/users and a request for POST /api/users traverse the same path in the tree but arrive at different handlers. The method matching happens inside the router's lookup logic, not in application code layered on top.

This architectural difference has practical consequences. For REST API servers where the same path serves multiple HTTP methods, find-my-way's integrated method support is a significant convenience — a single router.lookup(req, res) call dispatches based on both path and method with no additional code. For non-HTTP use cases where you're matching patterns that have no concept of methods — routing WebSocket message types, matching file-system glob patterns, or dispatching CLI subcommands — radix3's path-only model is actually cleaner. It does exactly what you need without method-matching overhead that is irrelevant to the use case. The right router is the one whose model matches your problem domain.

Migration Guide

From trek-router to find-my-way

Trek-router's add/find API maps cleanly to find-my-way's on/find:

// trek-router (old)
import Router from "trek-router"
const router = new Router()
router.add("GET", "/api/packages/:name", handler)
const [fn, params] = router.find("GET", "/api/packages/react")
// params → [{ name: "name", value: "react" }]

// find-my-way (new)
import FindMyWay from "find-my-way"
const router = FindMyWay()
router.on("GET", "/api/packages/:name", handler)
const result = router.find("GET", "/api/packages/react")
// result.params → { name: "react" }

Note the params shape change: trek-router returns an array of {name, value} objects; find-my-way returns a plain object keyed by parameter name.

From standalone find-my-way to Fastify

If you're running a raw http.createServer with find-my-way, migrating to Fastify preserves the routing model while adding schema validation, hooks, and plugins:

// Standalone find-my-way
import http from "node:http"
import FindMyWay from "find-my-way"
const router = FindMyWay()
router.on("GET", "/api/packages/:name", (req, res, params) => {
  res.end(JSON.stringify({ name: params.name }))
})
http.createServer((req, res) => router.lookup(req, res)).listen(3000)

// Equivalent Fastify (find-my-way runs internally)
import Fastify from "fastify"
const app = Fastify()
app.get("/api/packages/:name", async (req) => ({ name: req.params.name }))
app.listen({ port: 3000 })

Community Adoption in 2026

radix3 downloads have grown sharply alongside the UnJS ecosystem — Nuxt 3, H3, and Nitro all depend on it internally, pushing weekly downloads past 5 million even though most consumers never import it directly. The package is actively maintained by the UnJS team with frequent releases.

find-my-way sits at roughly 5 million weekly downloads, driven by Fastify's adoption. Every Fastify application installs find-my-way as a dependency. The package is co-maintained with Fastify core and follows the same release cadence. It is the most battle-tested of the three in high-throughput production environments.

trek-router downloads have declined steadily to around 50,000 per week, reflecting its minimal maintenance and the availability of better alternatives. The GitHub repository has seen no significant activity since 2020. For any new project, trek-router should be considered deprecated in practice even without a formal deprecation notice.

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on radix3 v1.x, find-my-way v8.x, and trek-router v1.x.

Compare HTTP routing and server frameworks on PkgPulse →

See also: h3 vs polka vs koa 2026 and proxy-agent vs global-agent vs hpagent, better-sqlite3 vs libsql vs sql.js.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.