A fast and minimal Trie based HTTP router.
The name comes from the Peepal (Sacred fig) tree, known for its deep roots and branching structure. Just like the tree, Peepal Router organizes routes in a Trie, so path matching stays fast and predictable with very low overhead.
- Trie based routing, zero dependencies, very small footprint
- Dynamic params, wildcards, and middleware chaining
- Works on any JavaScript engine — Node.js, Bun, Deno, and others
- TypeScript types included
npm install peepal-routerimport { TrieRouter } from "peepal-router";
const router = new TrieRouter();
router.add("GET", "/users/:id", () => "user profile");
const match = router.find("GET", "/users/42");
for (const fn of match.handler) fn();Create a router, register routes with add(), then look one up with find().
A match gives you back params, middlewares and handler. If nothing
matches, handler is undefined — that is your 404.
router.add("GET", "/users/:id/posts/:postId", () => "post");
router.find("GET", "/users/42/posts/7");
// params: { id: "42", postId: "7" }Any segment starting with : is a param. Peepal Router parses them for you
and hands them back in params. When a route has no params, params is
undefined instead of an empty object, so nothing is allocated for static
routes.
router.add("GET", "/static/*", () => "file");
// matches:
// /static/app.js
// /static/css/style.css
// /static/images/logo.png* matches the rest of the path, however many segments deep it goes.
// global — runs for every route
router.pushMiddleware("/", () => console.log("global"));
// whole subtree — runs for /users and everything under it
router.pushMiddleware("/users/*", () => console.log("users middleware"));
// that exact path only — not its children
router.pushMiddleware("/admin", () => console.log("admin middleware"));Middleware on / is global. Anything else is bound to that exact node, so
middleware on /admin does not run for /admin/settings. To cover a whole
subtree, mount it with a wildcard: /users/*. addMiddleware is an alias for
pushMiddleware.
import { TrieRouter, ALL_METHOD } from "peepal-router";
router.add(ALL_METHOD, "/health", () => "ok");ALL_METHOD registers a fallback for that path. It matches any method that
doesn't already have its own handler there.
router.pushMiddleware("/", globalMw);
router.pushMiddleware("/users/*", usersMw);
router.add("GET", "/users/:id", userHandler);
// compiled — middlewares already merged into handler
router.find("GET", "/users/42");
// { params: { id: "42" }, middlewares: undefined, handler: [globalMw, usersMw, userHandler] }
// walked on every call — kept separate
router.search("GET", "/users/42");
// { params: { id: "42" }, middlewares: [globalMw, usersMw], handler: [userHandler] }find() bakes each route's middlewares and handlers into one array ahead of
time, so middlewares is always undefined there — just run handler in
order. search() walks the trie on every call and keeps the two lists apart.
optimisedSearch() does the same as search() but parses the path without
split("/"), avoiding one array allocation.
Note: the first
find()call compiles the trie once and permanently switches the router over to the compiled lookup. Routes or middleware added after that won't be picked up until you callrouter.compile()again. Register everything before the firstfind(), or usesearch()/optimisedSearch()if you add routes at runtime.
import { TrieRouter } from "peepal-router";
const router = new TrieRouter();
router.pushMiddleware("/", () => console.log("global"));
router.pushMiddleware("/api/*", () => console.log("api middleware"));
router.add("GET", "/api/users/:id", () => console.log("user handler"));
const res = router.find("GET", "/api/users/100");
if (res.handler) {
for (const fn of res.handler) fn();
} else {
console.log("404");
}| Method | What it does |
|---|---|
add(method, path, handler | handler[]) |
Register a route. insert() is an alias. |
pushMiddleware(path, mw | mw[]) |
Register middleware. addMiddleware() is an alias. |
find(method, path) |
Compiled lookup. Middlewares merged into handler. |
search(method, path) |
Trie walk. Middlewares and handler kept separate. |
optimisedSearch(method, path) |
Same as search(), without the split("/") allocation. |
compile() |
Rebuild the compiled tree after adding routes post-find(). |
All lookups return the same shape:
{
params: Record<string, string> | undefined;
middlewares: Function[] | undefined;
handler: Function[] | undefined; // undefined = no match
}Peepal Router is built for speed and low allocation on the hot path:
- No unnecessary allocations while matching
- Minimal lookup overhead
- Fast static and dynamic matching
- Lightweight, cache friendly structure
- Route priority improvements
- Optional parameter support
- Regex based params
- Zero allocation path parser
- Extended benchmarking
Contributions, ideas, and performance improvements are welcome. Found a bug or a speedup? Open an issue or send a pull request.
MIT © Pradeep Kumar — github.com/pradeepbgs/peepal-router