0.2.4 โ€ข Published 4 months ago

@tradecrush/next-route-guard v0.2.4

Weekly downloads
-
License
MIT
Repository
github
Last release
4 months ago

@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.

npm version License: MIT Unit Tests Next.js Compatibility

Table of Contents

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

  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)
  1. 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"
  }
}
  1. 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).*)'
  ]
};
  1. 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):

ImplementationAvg time/requestSpeedup
Linear search0.271ms1ร—
Trie-based0.003ms90.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
โ”‚   โ”‚   โ”œโ”€โ”€ @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]])
  1. 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

0.2.3-3

5 months ago

0.2.3-2

5 months ago

0.2.3-1

5 months ago

0.2.4

4 months ago

0.2.1

5 months ago

0.2.0

5 months ago

0.2.3

5 months ago

0.2.2

5 months ago

0.1.4

5 months ago

0.1.3

5 months ago

0.1.2

5 months ago

0.1.1

5 months ago

0.1.0

5 months ago