@tradecrush/next-route-guard v0.2.4
@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
โcreateRouteGuardMiddleware
RouteAuthOptions
โ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.json
file 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.003ms
These 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/j
going from 0.175ms to 0.004ms) - Non-existent routes (387ร faster with
/unknown/path/not/found
going 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.tsx
This 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:
1. The URL is split into segments (e.g., /docs/api/auth
โ ["docs", "api", "auth"]
)
2. 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/api
matches the[...slug]
catch-all โ protected/docs/preview
matches an exact path with custom protection โ public/products/123/edit
has a nested protection override โ protected/help/admin
has a nested protection override โ protected/admin/users
matches the optional catch-all โ protected/admin
is protected as the base path for the catch-all/dashboard/profile
doesn't exist but falls under protected parent โ protected/about/team
doesn'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.tsx
Nested 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:
/about
is public (from parent(public)
)/docs/public-page
is public (from parent(public)
)/docs/admin
is protected (from nested(protected)
)/dashboard
is protected (from parent(protected)
)/settings/help
is 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): Middleware
RouteGuardOptions
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.json
This 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.json
Options:
--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 message
next-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.json
Options: 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.tsx
Mixed 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.tsx
In 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.tsx
Here, 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