next-middleware-router
v1.0.5
Published
Router for middleware in Next.js
Readme
next-middleware-router
Tiny, dependency-free path router for request guards in Next.js proxy.ts / middleware.ts.
It is designed for one job: take a pathname, match it against a nested route tree, run guard handlers from parent to child, and return the first value you abort with. In Next.js, that usually means returning a NextResponse.redirect(...) and falling back to NextResponse.next() when nothing aborts.
The library itself is framework agnostic. It only matches paths and executes synchronous handlers.
Installation
npm i next-middleware-routerpnpm add next-middleware-routerQuick Start
import { handleRoutes } from "next-middleware-router";
import { NextRequest, NextResponse } from "next/server";
export function proxy(req: NextRequest) {
const isLoggedIn = Boolean(req.cookies.get("session")?.value);
const isAdmin = req.cookies.get("role")?.value === "admin";
return (
handleRoutes(req.nextUrl.pathname, {
children: [
// Public routes that should bypass all auth logic
{
path: ["/api/*", "/favicon.ico"],
},
// Guest-only landing page
{
path: "/",
handler({ abort }) {
if (isLoggedIn) {
abort(NextResponse.redirect(new URL("/dashboard", req.url)));
}
},
},
// Everything else requires a session
{
path: "*",
handler({ abort }) {
if (!isLoggedIn) {
abort(NextResponse.redirect(new URL("/", req.url)));
}
},
children: [
{
path: "/dashboard",
},
{
path: "/admin/*",
handler({ abort }) {
if (!isAdmin) {
abort(NextResponse.redirect(new URL("/dashboard", req.url)));
}
},
},
],
},
],
}) ?? NextResponse.next()
);
}How It Works
handleRoutes() flattens your nested route config into a list of concrete matchers, sorts them from most specific to least specific, finds the first match, then runs the matched route's handler chain in parent-to-child order.
That makes it useful for guard stacks such as:
- public exceptions before auth checks
- "must be logged in" checks on a catch-all parent route
- role-based checks on nested subtrees
- guest-only redirects for login pages
API
handleRoutes(
pathname: string,
options?: {
basename?: string;
children?: RouteDef[];
},
): any | undefinedRouteDef
type RouteHandler = (args: {
params: Record<string, string>;
abort: (data: any) => void;
}) => void;
interface RouteDef {
path: string | string[];
handler?: RouteHandler;
caseSensitive?: boolean;
children?: RouteDef[];
}Arguments
pathname: the path to match, typicallyreq.nextUrl.pathnamebasename: an optional prefix stripped before route matchingchildren: the route tree
Return Value
- Returns the value passed to
abort(...) - Returns
undefinedif no handler aborts - Returns
undefinedif nothing matches
In Next.js, the common pattern is:
return handleRoutes(req.nextUrl.pathname, config) ?? NextResponse.next();Route Syntax
Routes are normalized internally, so leading and trailing slashes are optional. "/admin", "admin", "/admin/", and "admin/" all resolve to the same route.
Exact Segments
{
path: "/dashboard";
}Matches only /dashboard.
Named Params
{
path: "/users/:id";
}Matches /users/123 and exposes:
params.id === "123";Optional Params
{
path: "/users/:tab?";
}Matches both /users and /users/profile.
Wildcards
{
path: "/docs/*";
}Matches /docs, /docs/getting-started, and deeper paths.
The captured remainder is available as:
params["*"];For /docs/guides/setup, params["*"] is "guides/setup".
Wildcards are only valid at the end of a path. A route such as "/docs/*/edit" throws an error.
Optional Static Segments
{
path: "/reports/monthly?";
}Matches both /reports and /reports/monthly.
Multiple Paths Sharing One Handler
{
path: ["/login", "/register", "/forgot-password"],
handler({ abort }) {
// ...
},
}An array expands into multiple routes that share the same handler, children, and caseSensitive setting.
Nested Routes
Child routes are appended to the parent path, so the config stays compact and guard logic stays composable.
{
path: "/app",
handler({ abort }) {
// runs for every matched child route under /app
},
children: [
{ path: "/dashboard" }, // full route: /app/dashboard
{ path: "/settings" }, // full route: /app/settings
],
}A common guard pattern is a catch-all parent:
{
path: "*",
handler({ abort }) {
// require authentication for everything not matched earlier
},
children: [
{ path: "/dashboard" },
{ path: "/settings" },
],
}If a parent route ends with *, child routes are still built from the parent's prefix before the wildcard. For example, a parent of "/app/*" with a child "/admin" becomes "/app/admin".
Matching Rules
When multiple routes could match, the router chooses the most specific route:
- Exact segments
- Params (
:id) - Wildcards (
*)
Examples:
"/users/me"wins over"/users/:id""/users/:id"wins over"/users/*""/admin/settings"wins over"/admin/*"
Only one matched route chain runs.
Handler Execution
When a route matches, handlers execute from the outermost parent to the matched child.
{
path: "/admin",
handler() {
// 1st
},
children: [
{
path: "/users/:id",
handler() {
// 2nd
},
},
],
}All handlers in that chain receive the same params object.
That means a parent guard can inspect params defined by a deeper child route:
{
path: "/teams",
handler({ params }) {
// can read params.teamId if the matched child captured it
},
children: [
{ path: "/:teamId/settings" },
],
}abort(data)
Calling abort(data):
- stores
dataas the final return value - stops the remaining handlers in the current chain
- ends routing immediately
In Next.js, data is usually a NextResponse:
handler({ abort }) {
abort(NextResponse.redirect(new URL("/login", req.url)));
}basename
Use basename when the same route tree should only apply under a common prefix.
handleRoutes(req.nextUrl.pathname, {
basename: "/app",
children: [
{ path: "/" }, // matches /app
{ path: "/settings" }, // matches /app/settings
],
});If the incoming pathname does not match the basename, handleRoutes() returns undefined.
Framework-Agnostic Usage
The router is not tied to Next.js. abort() can return any value your application wants to use.
const result = handleRoutes("/admin/settings", {
children: [
{
path: "/admin/*",
handler({ abort }) {
const allowed = false;
if (!allowed) abort({ redirectTo: "/login", status: 302 });
},
},
],
});Practical Notes
- Handlers are synchronous. Do async work before calling
handleRoutes(). - Pass a pathname, not a full URL. In Next.js, use
req.nextUrl.pathname. - Matching is case-insensitive by default. Set
caseSensitive: trueper route when needed. - In nested trees,
caseSensitive: trueis effectively inherited by descendant routes. - URL params and wildcard values are
decodeURIComponent(...)decoded before they are exposed. - A route can exist only to group child guards;
handleris optional.
Real-World Patterns This Fits Well
These are the main usage patterns this library already supports well in production:
- redirect authenticated users away from
/,/login, or/register - require authentication for a catch-all subtree
- exempt public endpoints such as
/api/*, webhooks, file downloads, or OAuth callbacks - add role-based guards inside an already-authenticated subtree
- keep all path guard logic in one declarative tree instead of chained
ifstatements
License
MIT
