@tradecrush/next-route-guard
v0.2.4
Published
Convention-based route authentication middleware for Next.js applications
Downloads
22
Maintainers
Readme
@tradecrush/next-route-guard
🚀 NEW v0.2.4: Improved nested optional catch-all route handling, enhanced tests, fixed inconsistencies, and code cleanup
⚠️ BREAKING CHANGE: The primary function and types have been renamed:
createRouteAuthMiddleware→createRouteGuardMiddlewareRouteAuthOptions→RouteGuardOptions⚡ OPTIMIZED: Trie-based route matching (90× faster), improved optional catch-all route handling, and complete Next.js version compatibility!
A convention-based route authentication middleware for Next.js applications with App Router (Next.js 13.4.0 and up), fully tested and compatible with all major Next.js versions.
Table of Contents
- Features - Key capabilities and advantages
- Why Next Route Guard? - Problems solved and benefits
- Installation - How to add to your project
- Quick Start - Get up and running in minutes
- How It Works - Under the hood: build & runtime processes
- Route Protection Strategy - How routes are protected
- API Reference - Complete function and type documentation
- Package Exports - What's available in the package
- Development Mode - Tools for local development
- CLI Tools - Command-line utilities for route analysis
- Advanced Configuration - Customization options
- Example Scenarios - Common route protection patterns
- Compatibility - Supported Next.js versions
- License - MIT License information
Features
- 🔒 Convention-based Protection: Protect routes using directory naming conventions
- ⚡ Middleware-Based: Works with Next.js Edge middleware for fast authentication checks
- 🏗️ Build-time Analysis: Generates route maps during build for Edge runtime compatibility
- 🔄 Inheritance: Child routes inherit protection status from parent routes
- 🔀 Dynamic Routes: Full support for Next.js dynamic routes, catch-all routes, and optional segments
- ⚙️ Zero Runtime Overhead: Route protection rules are compiled at build time
- 🚀 Hyper-Optimized: Uses trie-based algorithms that are 90× faster than linear search
- 🛠️ Flexible Configuration: Customize authentication logic, redirection behavior, and more
- 👀 Watch Mode: Development tool that updates route maps as you add or remove routes
- ✅ Fully Compatible: Tested with Next.js 13.4.0, 14.0.0 and 15.0.0
Why Next Route Guard?
Next.js App Router is great, but it lacks a simple way to protect routes based on authentication. Next Route Guard solves this problem by providing a convention-based approach to route protection:
- No Duplicate Auth Logic: Define your auth rules once in middleware, not in every page
- Directory-Based: Organize routes naturally using Next.js route groups like
(public)and(protected) - Works with Any Auth Provider: Compatible with any authentication system (JWT, cookies, OAuth, etc.)
- Edge-Compatible: Works with Next.js Edge middleware for optimal performance
- TypeScript Support: Fully typed for excellent developer experience
Installation
npm install @tradecrush/next-route-guard
# or
yarn add @tradecrush/next-route-guard
# or
pnpm add @tradecrush/next-route-guard⭐ Quick Start
- Organize your routes using the
(public)and(protected)route groups:
app/
├── (public)/ # Public routes (no authentication required)
│ ├── login/
│ │ └── page.tsx
│ └── about/
│ └── page.tsx
├── (protected)/ # Protected routes (authentication required)
│ ├── dashboard/
│ │ └── page.tsx
│ └── settings/
│ └── page.tsx
└── layout.tsx # Root layout (applies to all routes)- Add the route map generation to your build script in package.json:
{
"scripts": {
"build": "next-route-guard-generate && next build",
"dev": "next-route-guard-watch & next dev"
}
}- Create a middleware.ts file in your project root:
// middleware.ts
import { createRouteGuardMiddleware } from '@tradecrush/next-route-guard';
import routeMap from './app/route-map.json';
import { NextResponse } from 'next/server';
export default createRouteGuardMiddleware({
routeMap,
isAuthenticated: async (request) => {
// Replace with your actual authentication logic
// This is just an example using cookies
const token = request.cookies.get('auth-token')?.value;
return !!token;
// Or using JWT from Authorization header
// const authHeader = request.headers.get('Authorization');
// return authHeader?.startsWith('Bearer ') || false;
},
onUnauthenticated: (request) => {
// Redirect to login with return URL
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('from', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
});
export const config = {
matcher: [
// Match all routes except static files, api routes, and other special paths
'/((?!_next/static|_next/image|favicon.ico).*)'
]
};- That's it! Your routes are now protected based on their directory structure.
How It Works
The package works in two stages:
1. Build Time: Route Analysis
During your build process, the next-route-guard-generate command:
- Scans your Next.js app directory structure
- Identifies routes and their protection status based on route groups
- Generates a static
route-map.jsonfile containing protected and public routes
2. Runtime: Middleware Protection
The middleware:
- Uses the generated route map to build an optimized route trie data structure
- Efficiently matches request paths against the trie to determine protection status
- Checks authentication status for protected routes
- Redirects unauthenticated users to login (or your custom logic)
- Allows direct access to public routes
Performance Benchmarks
Performance measurements with 1400 routes in the route map:
Routes: 1400
Average time per request: 0.003ms
Test path | Time per request
------------------------------------------|----------------
/public/page-250 | 0.002ms
/protected/page-499 | 0.004ms
/public/dynamic-50/12345 | 0.002ms
/protected/catch-25/a/b/c/d/e/f/g/h/i/j | 0.004ms
/protected/catch-49/a/b/c/edit | 0.002ms
/unknown/path/not/found | 0.003msThese benchmarks were run on Node.js v22.14.0 on a MacBook Pro (M3 Max), with 1000 requests per path.
Performance Comparison with Previous Version
Comparing to the previous linear search implementation (v0.1.4):
| Implementation | Avg time/request | Speedup | |----------------|------------------|---------| | Linear search | 0.271ms | 1× | | Trie-based | 0.003ms | 90.3× |
The trie-based implementation is 90.3× faster on average, with particular improvements for:
- Complex paths with many segments (43.8× faster for catch-all routes with
/protected/catch-25/a/b/c/d/e/f/g/h/i/jgoing from 0.175ms to 0.004ms) - Non-existent routes (387× faster with
/unknown/path/not/foundgoing from 1.162ms to 0.003ms)
Route Trie Optimization
Next Route Guard uses a specialized trie (prefix tree) data structure for route matching that dramatically improves performance:
- O(k) Matching Complexity: Routes are matched in time proportional to the path depth (k), not the total number of routes (n)
- Space-Efficient: Shared path prefixes are stored once in the tree structure
- Advanced Route Pattern Support: Optimized handling of all Next.js route patterns:
- Dynamic segments:
/users/[id] - Catch-all routes:
/docs/[...slug] - Optional catch-all:
/docs/[[...slug]] - Complex paths with rest segments:
/docs/[...slug]/edit - Multiple dynamic segments:
/products/[category]/[id]/details - Mixed dynamic and catch-all:
/articles/[section]/[...tags]/share
- Dynamic segments:
- One-time Initialization: The trie is built once when middleware initializes, then reused for all requests
- Consistent Performance: Lookup time remains stable regardless of route count (O(k) vs O(n×m))
- Protection Inheritance: Route protection statuses naturally flow through the tree structure
How the Route Trie Works
The route trie transforms your app directory structure into a tree representation that efficiently handles route protection. Let's look at a comprehensive example of a Next.js app directory with various route patterns:
app/
├── (public)/ # Public routes group
│ ├── about/
│ │ └── page.tsx # /about
│ ├── products/
│ │ ├── page.tsx # /products
│ │ └── [id]/
│ │ ├── page.tsx # /products/[id]
│ │ ├── reviews/
│ │ │ └── page.tsx # /products/[id]/reviews
│ │ └── (protected)/ # Nested protected group inside public
│ │ └── edit/
│ │ └── page.tsx # /products/[id]/edit (protected)
│ └── help/
│ ├── page.tsx # /help
│ └── (protected)/ # Nested protected group
│ └── admin/
│ └── page.tsx # /help/admin (protected)
├── (protected)/ # Protected routes group
│ ├── dashboard/
│ │ ├── page.tsx # /dashboard
│ │ ├── @stats/ # Parallel route
│ │ │ └── page.tsx # /dashboard/@stats
│ │ └── settings/
│ │ └── page.tsx # /dashboard/settings
│ ├── docs/
│ │ ├── page.tsx # /docs
│ │ ├── [...slug]/ # Required catch-all
│ │ │ └── page.tsx # /docs/[...slug]
│ │ └── (public)/ # Nested public group inside protected
│ │ └── preview/
│ │ └── page.tsx # /docs/preview (public)
│ └── admin/
│ ├── page.tsx # /admin (protected)
│ └── [[...slug]]/ # Optional catch-all (protects all subpaths)
│ └── page.tsx # /admin/settings, /admin/users, etc.
└── layout.tsxThis directory structure is converted to the following route trie:
/ (root)
├── about (public) # From (public)/about
├── products (public) # From (public)/products
│ └── [id] (dynamic - public) # From (public)/products/[id]
│ ├── reviews (public) # From (public)/products/[id]/reviews
│ └── edit (protected) # From (public)/products/[id]/(protected)/edit
├── help (public) # From (public)/help
│ └── admin (protected) # From (public)/help/(protected)/admin
├── dashboard (protected) # From (protected)/dashboard
│ ├── @stats (protected) # From (protected)/dashboard/@stats
│ └── settings (protected) # From (protected)/dashboard/settings
├── docs (protected) # From (protected)/docs
│ ├── [...slug] (protected) # From (protected)/docs/[...slug]
│ └── preview (public) # From (protected)/docs/(public)/preview
└── admin (protected) # From (protected)/admin
└── [[...slug]] (protected) # From (protected)/admin/[[...slug]]When a request arrives:
- The URL is split into segments (e.g.,
/docs/api/auth→["docs", "api", "auth"]) - The trie is traversed segment-by-segment, matching:
- Exact matches first (highest priority)
- Dynamic parameters next (e.g.,
[id]) - Catch-all segments as needed (e.g.,
[...slug],[[...optionalPath]])
- Protection status is determined from the matched node or parent nodes
/docs/apimatches the[...slug]catch-all → protected/docs/previewmatches an exact path with custom protection → public/products/123/edithas a nested protection override → protected/help/adminhas a nested protection override → protected/admin/usersmatches the optional catch-all → protected/adminis protected as the base path for the catch-all/dashboard/profiledoesn't exist but falls under protected parent → protected/about/teamdoesn't exist but falls under public parent → public
This approach provides orders of magnitude better performance than a linear search through route lists, especially for applications with many routes or complex routing patterns.
🔐 Route Protection Strategy
Next Route Guard uses Next.js Route Groups to determine which routes are protected and which are public.
Directory Conventions
- Routes in
(public)groups are public and don't require authentication - Routes in
(protected)groups are protected and require authentication - Routes inherit protection status from their parent directories
- Routes without an explicit protection status are protected by default (you can change this)
Custom Group Names
You can use custom group names instead of the default (public) and (protected):
npx next-route-guard-generate --app-dir ./app --output ./app/route-map.json --public "(open),(guest)" --protected "(auth),(admin)"This allows you to use groups like:
app/
├── (open)/ # Public routes (custom name)
│ ├── about/
│ └── signup/
├── (guest)/ # Also public routes (custom name)
│ └── features/
├── (auth)/ # Protected routes (custom name)
│ ├── dashboard/
│ └── settings/
├── (admin)/ # Also protected routes (custom name)
│ └── users/
├── layout.tsx
└── page.tsxNested Groups and Precedence
Nested groups take precedence over parent groups. This allows more fine-grained control:
app/
├── (public)/ # Public routes
│ ├── about/
│ ├── docs/
│ │ ├── public-page/
│ │ └── (protected)/ # Protected routes within public section
│ │ └── admin/
│ └── signup/
└── (protected)/ # Protected routes
├── dashboard/
└── settings/
└── (public)/ # Public routes within protected section
└── help/With this structure:
/aboutis public (from parent(public))/docs/public-pageis public (from parent(public))/docs/adminis protected (from nested(protected))/dashboardis protected (from parent(protected))/settings/helpis public (from nested(public))
📚 API Reference
The package provides several functions and types to help with route protection:
createRouteGuardMiddleware
The main function that creates a Next.js middleware function for route protection.
function createRouteGuardMiddleware(options: RouteGuardOptions): MiddlewareRouteGuardOptions
interface RouteGuardOptions {
/**
* Function to determine if a user is authenticated
*/
isAuthenticated: (request: NextRequest) => Promise<boolean> | boolean;
/**
* Function to handle unauthenticated requests
* Default: Redirects to /login with the original URL as a 'from' parameter
*/
onUnauthenticated?: (request: NextRequest) => Promise<NextResponse> | NextResponse;
/**
* Map of protected and public routes
*/
routeMap: RouteMap;
/**
* Default behavior for routes not in the route map
* Default: true (routes are protected by default)
*/
defaultProtected?: boolean;
/**
* URLs to exclude from authentication checks
* Default: ['/api/(.*)'] (excludes all API routes)
*/
excludeUrls?: (string | RegExp)[];
}Middleware Chaining
You can chain middleware functions to create a pipeline:
// middleware.ts
import { createRouteGuardMiddleware, chain } from '@tradecrush/next-route-guard';
import routeMap from './app/route-map.json';
import { NextResponse } from 'next/server';
// Logging middleware
const withLogging = (next) => {
return async (request) => {
console.log(`Request: ${request.method} ${request.url}`);
return next(request);
};
};
// Auth middleware
const withAuth = createRouteGuardMiddleware({
routeMap,
isAuthenticated: (request) => {
const token = request.cookies.get('token')?.value;
return !!token;
},
onUnauthenticated: (request) => {
const url = new URL('/login', request.url);
return NextResponse.redirect(url);
}
});
// Header middleware
const withHeaders = (next) => {
return async (request) => {
const response = await next(request);
if (response) {
response.headers.set('X-Powered-By', 'Next Route Guard');
}
return response;
};
};
// Export the middleware chain
export default chain([withLogging, withAuth, withHeaders]);
// Add a matcher
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};📦 Package Exports
The package exports the following:
{
// Main middleware creator
createRouteGuardMiddleware,
// Utility for chaining middleware
chain,
// Route map generator (for build scripts)
generateRouteMap,
// Types
type RouteGuardOptions,
type RouteMap,
type NextMiddleware
}🛠️ Development Mode
During development, you can use the watch mode to automatically update the route map when files change:
npx next-route-guard-watch --app-dir ./app --output ./app/route-map.jsonThis will watch for changes in your app directory and update the route map when files are added, modified, or deleted.
CLI Tools
The package includes two command-line tools to help manage your route maps:
next-route-guard-generate
Generates the route map file at build time:
# Basic usage with defaults
next-route-guard-generate
# With custom options
next-route-guard-generate --app-dir ./src/app --output ./src/lib/route-map.jsonOptions:
--app-dir <path> Path to the app directory (default: ./app)
--output <path> Path to the output JSON file (default: ./app/route-map.json)
--public <patterns> Comma-separated list of public route patterns (default: (public))
--protected <patterns> Comma-separated list of protected route patterns (default: (protected))
--help Display this help messagenext-route-guard-watch
Watches for route changes during development:
# Basic usage
next-route-guard-watch
# With custom options
next-route-guard-watch --app-dir ./src/app --output ./src/lib/route-map.jsonOptions: Same as next-route-guard-generate
Advanced Configuration
Excluding URLs
Some URL patterns can be excluded from authentication checks:
createRouteGuardMiddleware({
// ...
excludeUrls: [
'/api/(.*)', // Exclude API routes
'/images/(.*)', // Exclude static image paths
'/cdn-proxy/(.*)' // Exclude CDN proxy paths
]
});Default Protection Mode
By default, routes are protected unless explicitly marked as public. You can change this behavior:
createRouteGuardMiddleware({
// ...
defaultProtected: false // Routes are public by default
});This means routes without explicit protection groups will be treated as public.
Custom Authentication Logic
Implement your own authentication logic by providing an isAuthenticated function:
createRouteGuardMiddleware({
// ...
isAuthenticated: async (request) => {
// Check for a JWT in the Authorization header
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return false;
}
const token = authHeader.split(' ')[1];
try {
// Verify the token (using your preferred JWT library)
const payload = await verifyJwt(token);
return !!payload;
} catch (error) {
return false;
}
}
});Custom Redirection Behavior
Override the default redirection behavior:
createRouteGuardMiddleware({
// ...
onUnauthenticated: (request) => {
// Different behavior based on route type
const url = request.nextUrl.clone();
// If it's an API request, return a 401 response
if (request.nextUrl.pathname.startsWith('/api/')) {
return new NextResponse(
JSON.stringify({ error: 'Authentication required' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
// For dashboard routes, redirect to a custom login page
if (request.nextUrl.pathname.startsWith('/dashboard/')) {
url.pathname = '/dashboard-login';
url.searchParams.set('from', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
// Default login redirect
url.pathname = '/login';
url.searchParams.set('from', request.nextUrl.pathname);
return NextResponse.redirect(url);
}
});Example Scenarios
Simple Public/Protected Split
app/
├── (public)/
│ ├── login/
│ │ └── page.tsx
│ ├── register/
│ │ └── page.tsx
│ └── about/
│ └── page.tsx
└── (protected)/
├── dashboard/
│ └── page.tsx
└── profile/
└── page.tsxMixed Hierarchies
app/
├── (public)/
│ ├── help/
│ │ └── page.tsx
│ └── login/
│ └── page.tsx
├── dashboard/ # Protected (default)
│ ├── (public)/
│ │ └── preview/ # Public route inside a protected area
│ │ └── page.tsx
│ ├── overview/ # Protected
│ │ └── page.tsx
│ └── settings/ # Protected
│ └── page.tsx
└── layout.tsxIn this example, /dashboard/preview is public even though it's inside the protected /dashboard area.
Dynamic Routes
app/
├── (public)/
│ └── articles/
│ ├── page.tsx
│ └── [slug]/ # Public article pages
│ └── page.tsx
├── (protected)/
│ └── users/
│ ├── page.tsx
│ └── [id]/ # Protected user profiles
│ └── page.tsx
└── docs/ # Protected by default
├── [...slug]/ # Catch-all route
│ └── page.tsx
└── page.tsxHere, article pages with dynamic slugs are public, while user profiles with dynamic IDs are protected.
🧪 Compatibility
Next Route Guard is fully tested with the following Next.js versions:
- ✅ Next.js 13.4.0 (App Router initial release)
- ✅ Next.js 14.0.0
- ✅ Next.js 15.0.0
The middleware is optimized for the Edge runtime and uses efficient algorithms for route matching, making it suitable for production use with minimal overhead.
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Made with ❤️ by Tradecrush
