npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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-router
pnpm add next-middleware-router

Quick 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 | undefined

RouteDef

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, typically req.nextUrl.pathname
  • basename: an optional prefix stripped before route matching
  • children: the route tree

Return Value

  • Returns the value passed to abort(...)
  • Returns undefined if no handler aborts
  • Returns undefined if 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:

  1. Exact segments
  2. Params (:id)
  3. 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 data as 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: true per route when needed.
  • In nested trees, caseSensitive: true is 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; handler is 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 if statements

License

MIT