@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-discovererFeatures
- 🔍 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 directorydiscoverSingleRoute(filePath: string, basePath?: string): Promise<DiscoveredRoute | null>- Discover single routeanalyzeRouteFile(filePath: string, basePath?: string): Promise<RouteAnalysis | null>- Analyze route filediscoverNextJSRoutes(appDir?: string, pagesDir?: string): Promise<DiscoveredRoute[]>- Discover Next.js routesdiscoverExpressRoutes(routesDir: string): Promise<DiscoveredRoute[]>- Discover Express routesdiscoverFastifyRoutes(routesDir: string): Promise<DiscoveredRoute[]>- Discover Fastify routesvalidateRoute(route: DiscoveredRoute): ValidationResult- Validate routefilterRoutes(routes: DiscoveredRoute[], filters: RouteFilter[]): DiscoveredRoute[]- Filter routes
RouteAnalyzer
Utility class for route analysis.
Methods
analyzeRoutePattern(pattern: string): RoutePatternInfo- Analyze route patternextractParameters(routePath: string): ParameterInfo[]- Extract route parametersdetectConflicts(routes: DiscoveredRoute[]): RouteConflict[]- Detect route conflictsgenerateRouteTree(routes: DiscoveredRoute[]): RouteTree- Generate route treecalculateCoverage(routes: DiscoveredRoute[], patterns: string[]): CoverageReport- Calculate coverageanalyzeRoute(route: DiscoveredRoute): RouteAnalysis- Analyze single route
Framework Adapters
NextJSAdapter
detectFramework(filePath: string, content?: string): Promise<boolean>parseRoutePath(filePath: string, basePath: string): stringextractMethods(content: string): HttpMethod[]extractMetadata(content: string): RouteMetadataextractRouteSegments(path: string): RouteSegment[]
ExpressAdapter
detectFramework(filePath: string, content?: string): Promise<boolean>parseRoutePath(filePath: string, basePath: string): stringextractMethods(content: string): HttpMethod[]extractMetadata(content: string): RouteMetadataextractRouteDefinitions(content: string): RouteDefinition[]parseMiddleware(content: string): MiddlewareInfo[]
FastifyAdapter
detectFramework(filePath: string, content?: string): Promise<boolean>parseRoutePath(filePath: string, basePath: string): stringextractMethods(content: string): HttpMethod[]extractMetadata(content: string): RouteMetadataextractRouteDefinitions(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): booleanTypes
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
- @bernierllc/file-handler - File system operations used internally
- @bernierllc/jsdoc-openapi-parser - OpenAPI generation from JSDoc comments
- @bernierllc/nextjs-openapi - Next.js OpenAPI integration
License
Copyright (c) 2025 Bernier LLC. All rights reserved.
