@tradecrush/next-route-guard
Version:
Convention-based route authentication middleware for Next.js applications
692 lines (555 loc) โข 24.7 kB
Markdown
# /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.
[](https://www.npmjs.com/package/@tradecrush/next-route-guard)
[](LICENSE)
[](https://github.com/tradecrush/next-route-guard/actions/workflows/tests.yml)
[](https://github.com/tradecrush/next-route-guard/actions/workflows/compatibility.yml)
## Table of Contents
- [Features](#features) - Key capabilities and advantages
- [Why Next Route Guard?](#why-next-route-guard) - Problems solved and benefits
- [Installation](#installation) - How to add to your project
- [Quick Start](#-quick-start) - Get up and running in minutes
- [How It Works](#how-it-works) - Under the hood: build & runtime processes
- [Route Protection Strategy](#-route-protection-strategy) - How routes are protected
- [API Reference](#-api-reference) - Complete function and type documentation
- [Package Exports](#-package-exports) - What's available in the package
- [Development Mode](#-development-mode) - Tools for local development
- [CLI Tools](#cli-tools) - Command-line utilities for route analysis
- [Advanced Configuration](#advanced-configuration) - Customization options
- [Example Scenarios](#example-scenarios) - Common route protection patterns
- [Compatibility](#-compatibility) - Supported Next.js versions
- [License](#-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
```bash
npm install /next-route-guard
# or
yarn add /next-route-guard
# or
pnpm add /next-route-guard
```
## โญ Quick Start
1. **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)
```
2. **Add the route map generation to your build script** in package.json:
```json
{
"scripts": {
"build": "next-route-guard-generate && next build",
"dev": "next-route-guard-watch & next dev"
}
}
```
3. **Create a middleware.ts file** in your project root:
```typescript
// middleware.ts
import { createRouteGuardMiddleware } from '/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).*)'
]
};
```
4. **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`
- **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
โ โ โโโ / # Parallel route
โ โ โ โโโ page.tsx # /dashboard/
โ โ โโโ 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
โ โโโ (protected) # From (protected)/dashboard/
โ โโโ 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]]`)
3. 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](https://nextjs.org/docs/app/building-your-application/routing/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)`:
```bash
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.
```typescript
function createRouteGuardMiddleware(options: RouteGuardOptions): Middleware
```
#### RouteGuardOptions
```typescript
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:
```typescript
// middleware.ts
import { createRouteGuardMiddleware, chain } from '/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:
```typescript
{
// 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:
```bash
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:
```bash
# 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:
```bash
# 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:
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
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](LICENSE) file for details.
---
Made with โค๏ธ by [Tradecrush](https://www.tradecrush.io)