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

@bernierllc/route-discoverer

v0.6.0

Published

Framework-agnostic route discovery and analysis utilities for API route introspection

Readme

@bernierllc/route-discoverer

Framework-agnostic route discovery and analysis utilities for API route introspection across Next.js, Express, Fastify, and other web frameworks.

Installation

npm install @bernierllc/route-discoverer

Features

  • 🔍 Framework-Agnostic - Works with any file-based routing system
  • 🎯 Multi-Framework Support - Built-in adapters for Next.js (App Router & Pages Router), Express, and Fastify
  • 📊 Route Analysis - Extract parameters, detect conflicts, calculate coverage
  • 🏗️ Route Tree Generation - Build hierarchical route structures
  • Performance Optimized - Concurrent file processing with configurable limits
  • 🔒 Type-Safe - Full TypeScript support with detailed interfaces
  • 📝 Metadata Extraction - Parse JSDoc comments and inline metadata

Usage

Basic Route Discovery

import { RouteDiscoverer } from '@bernierllc/route-discoverer';

const discoverer = new RouteDiscoverer({
  patterns: ['**/*.{js,ts,jsx,tsx}'],
  ignore: ['**/*.test.{js,ts}', '**/node_modules/**'],
  recursive: true,
  maxDepth: 10
});

// Discover all routes in a directory
const routes = await discoverer.discoverRoutes('./src/api');

console.log(`Found ${routes.length} routes`);
routes.forEach(route => {
  console.log(`${route.methods.join(', ')} ${route.routePath} (${route.framework})`);
});

Framework-Specific Discovery

Next.js Routes

import { RouteDiscoverer } from '@bernierllc/route-discoverer';

const discoverer = new RouteDiscoverer({
  patterns: ['**/route.{js,ts}', '**/*.{js,ts,jsx,tsx}']
});

// Discover Next.js App Router and Pages Router routes
const routes = await discoverer.discoverNextJSRoutes(
  './app',      // App Router directory
  './pages/api' // Pages Router API directory (optional)
);

routes.forEach(route => {
  console.log(`${route.routePath}:`, route.methods);
  if (route.metadata.requiresAuth) {
    console.log('  ⚠️  Requires authentication');
  }
});

Express Routes

import { RouteDiscoverer } from '@bernierllc/route-discoverer';

const discoverer = new RouteDiscoverer({
  patterns: ['**/routes/**/*.{js,ts}']
});

const routes = await discoverer.discoverExpressRoutes('./src/routes');

routes.forEach(route => {
  console.log(`Express route: ${route.routePath}`);
});

Fastify Routes

import { RouteDiscoverer } from '@bernierllc/route-discoverer';

const discoverer = new RouteDiscoverer({
  patterns: ['**/routes/**/*.{js,ts}']
});

const routes = await discoverer.discoverFastifyRoutes('./src/routes');

routes.forEach(route => {
  console.log(`Fastify route: ${route.routePath}`);
});

Route Analysis

import { routeAnalyzer } from '@bernierllc/route-discoverer';

// Analyze route pattern
const patternInfo = routeAnalyzer.analyzeRoutePattern('/api/users/[id]/posts/[postId]');

console.log('Pattern depth:', patternInfo.depth);
console.log('Static segments:', patternInfo.staticSegments);
console.log('Dynamic segments:', patternInfo.dynamicSegments);
console.log('Specificity score:', patternInfo.specificity);

// Extract parameters
const parameters = routeAnalyzer.extractParameters('/api/users/[id]/posts/[postId]');

parameters.forEach(param => {
  console.log(`Parameter: ${param.name} (${param.type}, required: ${param.required})`);
});

// Detect conflicts between routes
const conflicts = routeAnalyzer.detectConflicts(routes);

conflicts.forEach(conflict => {
  console.log(`⚠️  Conflict detected (${conflict.type}):`);
  console.log(`   ${conflict.route1.routePath} vs ${conflict.route2.routePath}`);
  console.log(`   ${conflict.description}`);
  if (conflict.resolution) {
    console.log(`   💡 ${conflict.resolution}`);
  }
});

Generate Route Tree

import { routeAnalyzer } from '@bernierllc/route-discoverer';

const tree = routeAnalyzer.generateRouteTree(routes);

console.log(`Total routes: ${tree.totalRoutes}`);
console.log(`Max depth: ${tree.maxDepth}`);

// Traverse tree
function printTree(node, indent = '') {
  if (node.routes.length > 0) {
    console.log(`${indent}${node.path} (${node.routes.length} routes)`);
  }

  node.children.forEach(child => {
    printTree(child, indent + '  ');
  });
}

printTree(tree.root);

Calculate Coverage

import { routeAnalyzer } from '@bernierllc/route-discoverer';

const patterns = [
  '/api/users',
  '/api/users/[id]',
  '/api/posts',
  '/api/posts/[id]',
  '/api/comments'
];

const coverage = routeAnalyzer.calculateCoverage(routes, patterns);

console.log(`Coverage: ${coverage.coveragePercentage.toFixed(2)}%`);
console.log(`Covered: ${coverage.coveredPatterns}/${coverage.totalPatterns}`);

if (coverage.uncoveredPatterns.length > 0) {
  console.log('Uncovered patterns:');
  coverage.uncoveredPatterns.forEach(pattern => {
    console.log(`  - ${pattern}`);
  });
}

Single Route Analysis

import { RouteDiscoverer } from '@bernierllc/route-discoverer';

const discoverer = new RouteDiscoverer({
  patterns: ['**/*.{js,ts}']
});

const analysis = await discoverer.analyzeRouteFile('./app/api/users/[id]/route.ts');

if (analysis) {
  console.log('Route:', analysis.route.routePath);
  console.log('Methods:', analysis.route.methods);
  console.log('Is dynamic:', analysis.isDynamic);
  console.log('Is catch-all:', analysis.isCatchAll);
  console.log('Parameters:', analysis.parameters);
}

Route Validation

import { RouteDiscoverer } from '@bernierllc/route-discoverer';

const discoverer = new RouteDiscoverer({
  patterns: ['**/*.{js,ts}']
});

const routes = await discoverer.discoverRoutes('./src/api');

routes.forEach(route => {
  const validation = discoverer.validateRoute(route);

  if (!validation.isValid) {
    console.log(`❌ ${route.routePath}:`);
    validation.errors.forEach(error => {
      console.log(`   Error: ${error}`);
    });
  }

  if (validation.warnings.length > 0) {
    console.log(`⚠️  ${route.routePath}:`);
    validation.warnings.forEach(warning => {
      console.log(`   Warning: ${warning}`);
    });
  }
});

Route Filtering

import { RouteDiscoverer, FrameworkType, HttpMethod } from '@bernierllc/route-discoverer';

const discoverer = new RouteDiscoverer({
  patterns: ['**/*.{js,ts}']
});

const routes = await discoverer.discoverRoutes('./src/api');

// Filter by framework
const nextjsRoutes = discoverer.filterRoutes(
  routes,
  [route => route.framework === FrameworkType.NEXTJS]
);

// Filter by HTTP method
const postRoutes = discoverer.filterRoutes(
  routes,
  [route => route.methods.includes(HttpMethod.POST)]
);

// Filter by authentication requirement
const authRoutes = discoverer.filterRoutes(
  routes,
  [route => route.metadata.requiresAuth === true]
);

// Combine multiple filters
const securePostRoutes = discoverer.filterRoutes(routes, [
  route => route.methods.includes(HttpMethod.POST),
  route => route.metadata.requiresAuth === true,
  route => route.framework === FrameworkType.NEXTJS
]);

Using Framework Adapters Directly

import { NextJSAdapter, ExpressAdapter } from '@bernierllc/route-discoverer';

// Next.js adapter
const nextjs = new NextJSAdapter();

const isNextJS = await nextjs.detectFramework('./app/api/users/route.ts');
const routePath = nextjs.parseRoutePath('./app/api/users/[id]/route.ts', './app');
const segments = nextjs.extractRouteSegments('/api/users/[id]');

// Express adapter
const express = new ExpressAdapter();

const content = await fs.readFile('./routes/users.ts', 'utf-8');
const methods = express.extractMethods(content);
const metadata = express.extractMetadata(content);
const definitions = express.extractRouteDefinitions(content);

API Reference

RouteDiscoverer

Main class for discovering routes.

Constructor

constructor(options: RouteDiscovererOptions)

Options:

  • patterns: string[] - File patterns to match (glob patterns)
  • ignore?: string[] - Patterns to ignore (merged with defaults)
  • recursive?: boolean - Enable recursive directory traversal (default: true)
  • followSymlinks?: boolean - Follow symbolic links (default: false)
  • maxDepth?: number - Maximum directory depth (default: 20)

Methods

  • discoverRoutes(basePath: string): Promise<DiscoveredRoute[]> - Discover all routes in directory
  • discoverSingleRoute(filePath: string, basePath?: string): Promise<DiscoveredRoute | null> - Discover single route
  • analyzeRouteFile(filePath: string, basePath?: string): Promise<RouteAnalysis | null> - Analyze route file
  • discoverNextJSRoutes(appDir?: string, pagesDir?: string): Promise<DiscoveredRoute[]> - Discover Next.js routes
  • discoverExpressRoutes(routesDir: string): Promise<DiscoveredRoute[]> - Discover Express routes
  • discoverFastifyRoutes(routesDir: string): Promise<DiscoveredRoute[]> - Discover Fastify routes
  • validateRoute(route: DiscoveredRoute): ValidationResult - Validate route
  • filterRoutes(routes: DiscoveredRoute[], filters: RouteFilter[]): DiscoveredRoute[] - Filter routes

RouteAnalyzer

Utility class for route analysis.

Methods

  • analyzeRoutePattern(pattern: string): RoutePatternInfo - Analyze route pattern
  • extractParameters(routePath: string): ParameterInfo[] - Extract route parameters
  • detectConflicts(routes: DiscoveredRoute[]): RouteConflict[] - Detect route conflicts
  • generateRouteTree(routes: DiscoveredRoute[]): RouteTree - Generate route tree
  • calculateCoverage(routes: DiscoveredRoute[], patterns: string[]): CoverageReport - Calculate coverage
  • analyzeRoute(route: DiscoveredRoute): RouteAnalysis - Analyze single route

Framework Adapters

NextJSAdapter

  • detectFramework(filePath: string, content?: string): Promise<boolean>
  • parseRoutePath(filePath: string, basePath: string): string
  • extractMethods(content: string): HttpMethod[]
  • extractMetadata(content: string): RouteMetadata
  • extractRouteSegments(path: string): RouteSegment[]

ExpressAdapter

  • detectFramework(filePath: string, content?: string): Promise<boolean>
  • parseRoutePath(filePath: string, basePath: string): string
  • extractMethods(content: string): HttpMethod[]
  • extractMetadata(content: string): RouteMetadata
  • extractRouteDefinitions(content: string): RouteDefinition[]
  • parseMiddleware(content: string): MiddlewareInfo[]

FastifyAdapter

  • detectFramework(filePath: string, content?: string): Promise<boolean>
  • parseRoutePath(filePath: string, basePath: string): string
  • extractMethods(content: string): HttpMethod[]
  • extractMetadata(content: string): RouteMetadata
  • extractRouteDefinitions(content: string): RouteDefinition[]
  • extractPlugins(content: string): string[]

Helper Functions

// Route creation
createRouteDiscoverer(patterns?: string[], options?: Partial<RouteDiscovererOptions>): RouteDiscoverer

// Simple discovery wrapper
discoverRoutes(basePath: string, patterns?: string[]): Promise<PackageResult<DiscoveredRoute[]>>

// Path utilities
normalizePath(filePath: string): string
filePathToRoutePath(filePath: string, basePath: string): string
getPathDepth(filePath: string): number
getPathSegments(filePath: string): string[]
calculatePathSpecificity(routePath: string): number

// Pattern matching
matchesPattern(filePath: string, patterns: string[]): boolean
extractNextJSSegments(routePath: string): RouteSegment[]
nextJSToStandardRoute(routePath: string): string
routesConflict(route1: string, route2: string): boolean
isDynamicRoute(routePath: string): boolean
isCatchAllRoute(routePath: string): boolean

Types

DiscoveredRoute

interface DiscoveredRoute {
  filePath: string;           // Absolute path to route file
  relativePath: string;       // Relative path from base
  routePath: string;          // HTTP route path
  methods: HttpMethod[];      // Supported HTTP methods
  framework: FrameworkType;   // Detected framework
  metadata: RouteMetadata;    // Route metadata
  lastModified: Date;         // File modification time
}

RouteMetadata

interface RouteMetadata {
  description?: string;       // Route description
  tags?: string[];           // Route tags/categories
  requiresAuth?: boolean;    // Authentication required
  rateLimit?: {              // Rate limiting config
    requests: number;
    window: number;
  };
  [key: string]: unknown;    // Custom metadata
}

RouteAnalysis

interface RouteAnalysis {
  route: DiscoveredRoute;         // The analyzed route
  parameters: ParameterInfo[];    // Extracted parameters
  patternInfo: RoutePatternInfo;  // Pattern information
  isDynamic: boolean;             // Has dynamic segments
  isCatchAll: boolean;            // Is catch-all route
}

Integration Status

  • Logger integration: not-applicable - Core utility package with minimal logging needs (no @bernierllc/logger dependency required)
  • Docs-Suite: ready - Comprehensive JSDoc comments and TypeDoc-compatible API documentation
  • NeverHub integration: not-applicable - Standalone utility package with no service dependencies (no @bernierllc/neverhub-adapter required)

Performance

The package is optimized for large codebases:

  • Concurrent Processing: Files are processed in batches (default: 50 concurrent reads)
  • File Size Limits: Files over 10MB are automatically skipped
  • Route Limits: Supports up to 10,000 routes per discovery
  • Depth Limits: Configurable maximum directory depth (default: 20 levels)

Typical performance:

  • 1,000 routes: < 5 seconds
  • 5,000 routes: < 20 seconds
  • 10,000 routes: < 40 seconds

See Also

License

Copyright (c) 2025 Bernier LLC. All rights reserved.