@raven-js/wings
Version:
Zero-dependency isomorphic routing library for modern JavaScript - Server and CLI routing
873 lines (829 loc) • 29.1 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* @file High-performance HTTP request router using Trie data structure for optimal route matching.
*
* Provides the Router class for handling HTTP requests with middleware support, path parameters,
* and method chaining. Works in Node.js, browser, and serverless environments.
*/
import {
getHttpMethods,
HTTP_METHODS,
isValidHttpMethod,
} from "./http-methods.js";
import { Middleware } from "./middleware.js";
import { Route } from "./route.js";
import { Trie } from "./trie.js";
/**
* HTTP request router with Trie-based route matching and middleware support.
*
* @example
* // Basic router setup
* const router = new Router();
* router.get('/users', async (ctx) => ctx.json(users));
* router.post('/users', async (ctx) => ctx.json(newUser));
*
* @example
* // Router with middleware
* const router = new Router();
* router.get('/admin/*', adminHandler).before(auth);
*/
export class Router {
/**
* Internal storage for all registered routes.
*
* This array stores all Route instances in the order they were added.
* The array is append-only, making it safe to use positional indices
* as unique route IDs for the Trie data structure.
*
* **Note**: Routes are never removed from this array, only added.
* This ensures consistent indexing for the Trie lookup system.
*
* @type {import('./route.js').Route[]}
*/
#routes = [];
/**
* Trie data structures for fast route matching.
*
* This object contains separate Trie instances for each HTTP method,
* enabling O(1) route matching performance. Each Trie is optimized
* for the specific patterns and constraints of its HTTP method.
*
* **Structure**: `{ [method]: Trie }` where method is a valid HTTP method
* **V8 optimization**: Object.create(null) eliminates prototype chain
* for faster method lookup and cleaner object shapes.
*
* @type {Object<string, Trie>}
*/
#tries = Object.create(null);
/**
* Internal storage for registered middleware.
*
* This array stores all Middleware instances that will be executed
* for every request handled by this router. Middleware is executed
* in the order it was added.
*
* **Note**: Middleware with duplicate identifiers are automatically
* filtered out to prevent duplicate execution.
*
* @type {Middleware[]}
*/
#middlewares = [];
/**
* Set of middleware identifiers for O(1) duplicate detection.
*
* This Set tracks middleware identifiers to enable constant-time
* duplicate checking instead of O(n) linear search through the
* middlewares array.
*
* **Performance**: O(1) lookup vs O(n) array iteration for duplicate detection.
*
* @type {Set<string>}
*/
#middlewareIdentifiers = new Set();
/**
* Cache for normalized path segments.
*
* @type {Map<string, string[]>}
*/
#pathSegmentsCache = new Map();
/**
* Creates a new Router instance with HTTP method tries pre-initialized.
*
* @example
* // Basic router creation
* const router = new Router();
*
* @example
* // Router with immediate route registration
* const router = new Router();
* router.get('/api/users', handleUsers);
*/
constructor() {
// Initialize all HTTP method tries upfront for better performance
getHttpMethods().forEach((method) => {
this.#tries[method] = new Trie();
});
}
/**
* Private helper method to register a route in the trie and add it to the routes array.
*
* This method handles the internal registration process by:
* 1. Normalizing the path for consistent matching
* 2. Registering the route in the appropriate Trie for the HTTP method
* 3. Adding the route to the routes array for later retrieval
*
* **Performance**: This operation is O(n) where n is the number of path segments.
*
* @param {import('./http-methods.js').HttpMethod} method - The HTTP method
* @param {string} path - The path of the route
* @param {import('./route.js').Route} route - The route instance to add
* @returns {Router} The Router instance for chaining
*/
#registerRoute(method, path, route) {
this.#tries[method].register(
this.#getPathSegments(path),
this.#routes.length,
);
this.#routes.push(route);
return this;
}
/**
* Private helper method to add a route for a specific HTTP method.
*
* This method creates a Route instance using the appropriate factory method
* and then registers it using the #registerRoute helper. It provides a
* consistent interface for all HTTP method route additions.
*
* @param {import('./http-methods.js').HttpMethod} method - The HTTP method
* @param {string} path - The path of the route
* @param {import('./middleware.js').Handler} handler - The route handler function
* @returns {Router} The Router instance for chaining
*/
#addMethodRoute(method, path, handler) {
const route = /** @type {any} */ (Route)[method](path, handler);
return this.#registerRoute(method, path, route);
}
/**
* Adds a new GET route to the router.
*
* This method creates and registers a GET route with the specified path
* and handler function. GET routes are typically used for retrieving
* resources and should be idempotent and safe.
*
* **GET Method**: Used for retrieving resources. GET requests should not
* modify server state and can be cached safely.
*
* @param {string} path - The path pattern for the route (e.g., '/users/:id')
* @param {import('./middleware.js').Handler} handler - The function to handle GET requests
* @returns {Router} The Router instance for method chaining
*
* @example
* ```javascript
* // Basic GET route
* router.get('/users', (ctx) => {
* ctx.json({ users: getAllUsers() });
* });
*
* // GET route with path parameters
* router.get('/users/:id', (ctx) => {
* const userId = ctx.pathParams.id;
* const user = getUserById(userId);
* ctx.json(user);
* });
*
* // Method chaining
* router
* .get('/users', listUsers)
* .get('/users/:id', getUser)
* .get('/users/:id/posts', getUserPosts);
* ```
*/
get(path, handler) {
return this.#addMethodRoute(HTTP_METHODS.GET, path, handler);
}
/**
* Adds a new POST route to the router.
*
* This method creates and registers a POST route with the specified path
* and handler function. POST routes are typically used for creating
* new resources.
*
* **POST Method**: Used for creating new resources. POST requests
* typically include a request body with the data to create.
*
* @param {string} path - The path pattern for the route (e.g., '/users')
* @param {import('./middleware.js').Handler} handler - The function to handle POST requests
* @returns {Router} The Router instance for method chaining
*
* @example
* ```javascript
* // Basic POST route
* router.post('/users', async (ctx) => {
* const userData = ctx.requestBody();
* const newUser = await createUser(userData);
* ctx.json(newUser);
* });
*
* // POST route with validation
* router.post('/users', async (ctx) => {
* const userData = ctx.requestBody();
* if (!userData.name || !userData.email) {
* ctx.responseStatusCode = 400;
* ctx.json({ error: 'Name and email are required' });
* return;
* }
* const newUser = await createUser(userData);
* ctx.json(newUser);
* });
* ```
*/
post(path, handler) {
return this.#addMethodRoute(HTTP_METHODS.POST, path, handler);
}
/**
* Adds a new PUT route to the router.
*
* This method creates and registers a PUT route with the specified path
* and handler function. PUT routes are typically used for replacing
* entire resources.
*
* **PUT Method**: Used for replacing entire resources. PUT requests
* should be idempotent and typically include a complete resource
* representation in the request body.
*
* @param {string} path - The path pattern for the route (e.g., '/users/:id')
* @param {import('./middleware.js').Handler} handler - The function to handle PUT requests
* @returns {Router} The Router instance for method chaining
*
* @example
* ```javascript
* // Basic PUT route
* router.put('/users/:id', async (ctx) => {
* const userId = ctx.pathParams.id;
* const userData = ctx.requestBody();
* const updatedUser = await updateUser(userId, userData);
* ctx.json(updatedUser);
* });
* ```
*/
put(path, handler) {
return this.#addMethodRoute(HTTP_METHODS.PUT, path, handler);
}
/**
* Adds a new DELETE route to the router.
*
* This method creates and registers a DELETE route with the specified path
* and handler function. DELETE routes are typically used for removing
* resources.
*
* **DELETE Method**: Used for removing resources. DELETE requests
* should be idempotent and typically don't include a request body.
*
* @param {string} path - The path pattern for the route (e.g., '/users/:id')
* @param {import('./middleware.js').Handler} handler - The function to handle DELETE requests
* @returns {Router} The Router instance for method chaining
*
* @example
* ```javascript
* // Basic DELETE route
* router.delete('/users/:id', async (ctx) => {
* const userId = ctx.pathParams.id;
* await deleteUser(userId);
* ctx.responseStatusCode = 204; // No Content
* });
* ```
*/
delete(path, handler) {
return this.#addMethodRoute(HTTP_METHODS.DELETE, path, handler);
}
/**
* Adds a new PATCH route to the router.
*
* This method creates and registers a PATCH route with the specified path
* and handler function. PATCH routes are typically used for partially
* updating resources.
*
* **PATCH Method**: Used for partially updating resources. PATCH requests
* should be idempotent and typically include only the fields to update
* in the request body.
*
* @param {string} path - The path pattern for the route (e.g., '/users/:id')
* @param {import('./middleware.js').Handler} handler - The function to handle PATCH requests
* @returns {Router} The Router instance for method chaining
*
* @example
* ```javascript
* // Basic PATCH route
* router.patch('/users/:id', async (ctx) => {
* const userId = ctx.pathParams.id;
* const updates = ctx.requestBody();
* const updatedUser = await updateUserPartial(userId, updates);
* ctx.json(updatedUser);
* });
* ```
*/
patch(path, handler) {
return this.#addMethodRoute(HTTP_METHODS.PATCH, path, handler);
}
/**
* Adds a new HEAD route to the router.
*
* This method creates and registers a HEAD route with the specified path
* and handler function. HEAD routes are typically used for retrieving
* headers only, without the response body.
*
* **HEAD Method**: Used for retrieving headers only, without the response body.
* HEAD requests are useful for checking if a resource exists or getting
* metadata without transferring the full content.
*
* @param {string} path - The path pattern for the route (e.g., '/users/:id')
* @param {import('./middleware.js').Handler} handler - The function to handle HEAD requests
* @returns {Router} The Router instance for method chaining
*
* @example
* ```javascript
* // Basic HEAD route
* router.head('/users/:id', async (ctx) => {
* const userId = ctx.pathParams.id;
* const exists = await userExists(userId);
* if (!exists) {
* ctx.responseStatusCode = 404;
* }
* // No response body for HEAD requests
* });
* ```
*/
head(path, handler) {
return this.#addMethodRoute(HTTP_METHODS.HEAD, path, handler);
}
/**
* Adds a new OPTIONS route to the router.
*
* This method creates and registers an OPTIONS route with the specified path
* and handler function. OPTIONS routes are typically used for discovering
* the allowed HTTP methods and other capabilities of a resource.
*
* **OPTIONS Method**: Used for discovering the allowed HTTP methods
* and other capabilities of a resource. OPTIONS requests are commonly
* used for CORS preflight requests.
*
* @param {string} path - The path pattern for the route (e.g., '/users')
* @param {import('./middleware.js').Handler} handler - The function to handle OPTIONS requests
* @returns {Router} The Router instance for method chaining
*
* @example
* ```javascript
* // Basic OPTIONS route
* router.options('/users', (ctx) => {
* ctx.responseHeaders.set('access-control-allow-methods', 'GET, POST, PUT, DELETE');
* ctx.responseHeaders.set('access-control-allow-headers', 'Content-Type, Authorization');
* ctx.responseHeaders.set('access-control-max-age', '86400');
* ctx.responseStatusCode = 204;
* });
* ```
*/
options(path, handler) {
return this.#addMethodRoute(HTTP_METHODS.OPTIONS, path, handler);
}
/**
* Adds a new COMMAND route to the router.
*
* This method creates and registers a COMMAND route with the specified path
* and handler function. COMMAND routes are used for CLI command execution,
* enabling unified handling of both HTTP requests and CLI operations.
*
* **COMMAND Method**: Used for CLI command execution. COMMAND routes
* handle terminal commands through the Wings routing system, allowing
* the same middleware and routing logic to work for both web and CLI.
*
* @param {string} path - The command path pattern (e.g., '/git/commit')
* @param {import('./middleware.js').Handler} handler - The function to handle COMMAND requests
* @returns {Router} The Router instance for method chaining
*
* @example
* ```javascript
* // Basic COMMAND route
* router.cmd('/git/commit', async (ctx) => {
* const message = ctx.queryParams.get('message') || 'Default commit message';
* await executeGitCommit(message);
* ctx.text('✅ Committed successfully');
* });
*
* // COMMAND route with path parameters
* router.cmd('/deploy/:environment', (ctx) => {
* const env = ctx.pathParams.environment;
* console.log(`Deploying to ${env}...`);
* ctx.text(`✅ Deployed to ${env}`);
* });
*
* // Method chaining
* router
* .cmd('/git/status', gitStatusHandler)
* .cmd('/git/commit', gitCommitHandler)
* .cmd('/deploy/:env', deployHandler);
* ```
*/
cmd(path, handler) {
return this.#addMethodRoute(HTTP_METHODS.COMMAND, path, handler);
}
/**
* Adds a pre-configured Route instance to the router.
*
* This method provides a more flexible way to add routes by accepting
* a complete Route instance. It's useful when you need to add routes
* with custom middleware, constraints, or descriptions that were
* created using the Route factory methods.
*
* **Validation**: The route's HTTP method must be one of the supported
* methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS).
*
* @param {import('./route.js').Route} route - The Route instance to add
* @returns {Router} The Router instance for method chaining
* @throws {Error} If the route method is not a supported HTTP method
*
* @example
* ```javascript
* // Create a route with custom options
* const userRoute = Route.GET('/users/:id', (ctx) => {
* ctx.json({ id: ctx.pathParams.id });
* }, {
* middleware: [authMiddleware],
* constraints: { id: '\\d+' },
* description: 'Get user by ID'
* });
*
* // Add the pre-configured route
* router.addRoute(userRoute);
*
* // Create multiple routes and add them
* const routes = [
* Route.POST('/users', createUserHandler),
* Route.PUT('/users/:id', updateUserHandler),
* Route.DELETE('/users/:id', deleteUserHandler)
* ];
*
* routes.forEach(route => router.addRoute(route));
*
* // Error case - unsupported method
* try {
* const invalidRoute = { method: 'INVALID', path: '/test', handler: () => {} };
* router.addRoute(invalidRoute);
* } catch (error) {
* console.error(error.message); // "Unsupported HTTP method: INVALID"
* }
* ```
*/
addRoute(route) {
const method = route.method;
// Validate that the method is supported
if (!isValidHttpMethod(method)) {
throw new Error(
`Unsupported HTTP method: ${method}. Supported methods: ${Object.values(HTTP_METHODS).join(", ")}`,
);
}
// Use the helper method for the common logic
return this.#registerRoute(method, route.path, route);
}
/**
* Adds middleware to the end of the middleware chain.
*
* This method appends middleware to the router's middleware array.
* Middleware added with this method will be executed before the route
* handler in the order they were added.
*
* **Duplicate Prevention**: Middleware with the same identifier will
* not be added multiple times, preventing duplicate execution.
*
* **Execution Order**: Middleware added with `use()` runs after
* middleware added with `useEarly()`.
*
* @param {import('./middleware.js').Middleware} middleware - The middleware instance to add
* @returns {Router} The Router instance for method chaining
*
* @example
* ```javascript
* // Add authentication middleware
* const authMiddleware = new Middleware(async (ctx) => {
* const token = ctx.requestHeaders.get('authorization');
* if (!token) {
* ctx.responseStatusCode = 401;
* ctx.responseEnded = true;
* return;
* }
* ctx.data.user = await validateToken(token);
* }, 'authentication');
*
* router.use(authMiddleware);
*
* // Add logging middleware
* const loggingMiddleware = new Middleware((ctx) => {
* console.log(`${ctx.method} ${ctx.path} - ${new Date().toISOString()}`);
* }, 'logging');
*
* router.use(loggingMiddleware);
*
* // Method chaining
* router
* .use(authMiddleware)
* .use(loggingMiddleware)
* .get('/users', userHandler);
*
* // Duplicate prevention
* router.use(authMiddleware); // Won't add duplicate
* router.use(authMiddleware); // Won't add duplicate
* ```
*/
use(middleware) {
// Check for duplicate identifiers using O(1) Set lookup
if (
middleware.identifier &&
this.#middlewareIdentifiers.has(middleware.identifier)
) {
return this;
}
this.#middlewares.push(middleware);
if (middleware.identifier) {
this.#middlewareIdentifiers.add(middleware.identifier);
}
return this;
}
/**
* Adds middleware to the beginning of the middleware chain.
*
* This method prepends middleware to the router's middleware array.
* Middleware added with this method will be executed before the route
* handler and before middleware added with `use()`.
*
* **Duplicate Prevention**: Middleware with the same identifier will
* not be added multiple times, preventing duplicate execution.
*
* **Execution Order**: Middleware added with `useEarly()` runs before
* middleware added with `use()`.
*
* @param {import('./middleware.js').Middleware} middleware - The middleware instance to add
* @returns {Router} The Router instance for method chaining
*
* @example
* ```javascript
* // Add CORS middleware early (should run first)
* const corsMiddleware = new Middleware((ctx) => {
* ctx.responseHeaders.set('access-control-allow-origin', '*');
* ctx.responseHeaders.set('access-control-allow-methods', 'GET, POST, PUT, DELETE');
* }, 'cors');
*
* router.useEarly(corsMiddleware);
*
* // Add authentication middleware (runs after CORS)
* const authMiddleware = new Middleware(async (ctx) => {
* // Authentication logic
* }, 'authentication');
*
* router.use(authMiddleware);
*
* // Execution order: cors -> auth -> handler
* router.get('/users', userHandler);
*
* // Method chaining
* router
* .useEarly(corsMiddleware)
* .use(authMiddleware)
* .get('/users', userHandler);
* ```
*/
useEarly(middleware) {
// Check for duplicate identifiers using O(1) Set lookup
if (
middleware.identifier &&
this.#middlewareIdentifiers.has(middleware.identifier)
) {
return this;
}
this.#middlewares.unshift(middleware);
if (middleware.identifier) {
this.#middlewareIdentifiers.add(middleware.identifier);
}
return this;
}
/**
* Private method to match a request against registered routes.
*
* This method uses the Trie data structure to find the best matching
* route for the given HTTP method and request path. It returns both
* the matched route and any extracted path parameters.
*
* **Performance**: O(m) where m is the number of segments in the request path.
* The total number of registered routes is irrelevant for matching performance.
*
* @param {string} method - The HTTP method of the request
* @param {string} requestPath - The path of the request
* @returns {{route: import('./route.js').Route | undefined, params: Object<string, string>}} Object containing the matched route and path parameters
*/
#match(method, requestPath) {
const pathSegments = this.#getPathSegments(requestPath);
const trie = this.#tries[method];
if (!trie) return { route: undefined, params: {} };
const { id, params } = this.#tries[method].match(pathSegments);
if (id < 0) return { route: undefined, params };
return { route: this.#routes[id], params };
}
/**
* Private method to normalize path strings for consistent matching.
*
* This method removes leading and trailing slashes from paths to ensure
* consistent route matching regardless of how the path was originally
* specified.
*
* @param {string} path - The path to normalize
* @returns {string} The normalized path
*/
#normalizePath(path) {
let p = path;
p = p.replace(/^\/|\/$/g, "");
return p;
}
/**
* Optimized method to get normalized path segments with caching.
*
* This method combines path normalization and splitting with caching
* to avoid repeated string operations for commonly accessed paths.
*
* **Performance**: O(1) for cached paths vs O(n) string operations.
* **Memory**: LRU cache with 100 entry limit to prevent memory leaks.
*
* @param {string} path - The path to normalize and split
* @returns {string[]} Array of normalized path segments
*/
#getPathSegments(path) {
// Check cache first
if (this.#pathSegmentsCache.has(path)) {
return this.#pathSegmentsCache.get(path);
}
// Normalize and split the path
const normalizedPath = this.#normalizePath(path);
const segments = normalizedPath.split("/");
// Cache with size limit (simple LRU: delete oldest when limit reached)
if (this.#pathSegmentsCache.size >= 100) {
const firstKey = this.#pathSegmentsCache.keys().next().value;
this.#pathSegmentsCache.delete(firstKey);
}
this.#pathSegmentsCache.set(path, segments);
return segments;
}
/**
* Handles an incoming HTTP request through the complete middleware and routing pipeline.
*
* This method orchestrates the entire request processing flow with enhanced error collection:
* 1. Executes all registered middleware (before callbacks)
* 2. Matches the request against registered routes
* 3. Executes the matched route handler
* 4. Executes any after callbacks (always runs, even if errors occurred)
* 5. Handles errors gracefully by collecting them and throwing the first one
*
* **Request Flow**:
* - Middleware execution (before) [errors collected]
* - Route matching and parameter extraction
* - Route handler execution [errors collected]
* - Middleware execution (after) [errors collected, always runs]
* - Error throwing (if any errors were collected)
*
* **Error Collection**: Errors from any step are collected in `ctx.errors` instead
* of immediately throwing. This ensures that after callbacks (like logging) always
* run, providing complete request lifecycle tracking even when errors occur.
*
* **Error Handling**: If any errors are collected during the request lifecycle,
* a 500 response is set but no errors are thrown. Middleware (like logger) can
* consume errors from `ctx.errors` for formatting. Any remaining unconsumed
* errors are printed to console.error as a fallback.
*
* **Context Mutation**: This method modifies the provided Context instance
* directly and returns it for convenience.
*
* @param {import('./context.js').Context} ctx - The HTTP lifecycle context
* @returns {Promise<import('./context.js').Context>} The modified context instance
*
* @example
* ```javascript
* // Create router with routes and middleware
* const router = new Router();
* router.use(authMiddleware);
* router.get('/users/:id', async (ctx) => {
* const userId = ctx.pathParams.id;
* const user = await getUserById(userId);
* ctx.json(user);
* });
*
* // Handle a request
* const url = new URL('http://localhost/users/123');
* const ctx = new Context('GET', url, new Headers());
*
* const result = await router.handleRequest(ctx);
*
* // Check the response
* console.log(result.responseStatusCode); // 200
* console.log(result.responseBody); // JSON string with user data
* console.log(result.errors); // [] (empty array, no errors)
*
* // Handle a non-existent route
* const notFoundUrl = new URL('http://localhost/nonexistent');
* const notFoundCtx = new Context('GET', notFoundUrl, new Headers());
*
* const notFoundResult = await router.handleRequest(notFoundCtx);
* console.log(notFoundResult.responseStatusCode); // 404
*
* // Handle a request that causes an error
* const errorUrl = new URL('http://localhost/users/invalid');
* const errorCtx = new Context('GET', errorUrl, new Headers());
*
* const errorResult = await router.handleRequest(errorCtx);
* console.log(errorResult.responseStatusCode); // 500 (error response set)
* console.log(errorResult.errors.length); // 0 (if logger consumed the errors)
* // Error would be logged in formatted output and then consumed by logger
* ```
*/
async handleRequest(ctx) {
// run all before hooks first on the context instance
ctx.addBeforeCallbacks(this.#middlewares);
try {
await ctx.runBeforeCallbacks();
} catch (error) {
ctx.errors.push(error);
}
if (ctx.responseEnded) return ctx;
// try to find a route that matches the request
const { route, params } = this.#match(ctx.method, ctx.path);
if (route) {
// run the handler with the extracted pathParams
ctx.pathParams = params;
try {
await route.handler(ctx);
} catch (error) {
ctx.errors.push(error);
}
} else {
// no route found - set 404 but don't return early
await ctx.notFound();
}
if (ctx.responseEnded) return ctx;
// set 500 status if errors occurred, before running after callbacks
// so logger middleware sees the correct status code
if (ctx.errors.length > 0 && !ctx.responseEnded) {
await ctx.error();
}
// run all after hooks regardless of previous errors
try {
await ctx.runAfterCallbacks();
} catch (error) {
ctx.errors.push(error);
// Set 500 status if new errors occurred during after callbacks
if (!ctx.responseEnded) {
await ctx.error();
}
}
// print any remaining errors that weren't consumed by middleware (like logger)
if (ctx.errors.length > 0) {
ctx.errors.forEach((error) => {
console.error(error);
});
// Clear the errors array after printing them
ctx.errors.length = 0;
}
return ctx;
}
/**
* Lists all registered routes, optionally filtered by HTTP method.
*
* This method returns an array of all Route instances that have been
* registered with the router. You can optionally filter the results
* by specifying an HTTP method.
*
* **Note**: The returned array is a copy of the internal routes array,
* so modifying it won't affect the router's internal state.
*
* @param {string} [method] - Optional HTTP method to filter routes (e.g., 'GET', 'POST')
* @returns {import('./route.js').Route[]} Array of registered routes
*
* @example
* ```javascript
* // Get all routes
* const allRoutes = router.listRoutes();
* console.log(`Total routes: ${allRoutes.length}`);
*
* // Get routes for specific method
* const getRoutes = router.listRoutes('GET');
* console.log(`GET routes: ${getRoutes.length}`);
*
* // Get routes for another method
* const postRoutes = router.listRoutes('POST');
* console.log(`POST routes: ${postRoutes.length}`);
*
* // Iterate over routes
* router.listRoutes().forEach(route => {
* console.log(`${route.method} ${route.path}`);
* });
*
* // Filter routes by method
* const userRoutes = router.listRoutes().filter(route =>
* route.path.startsWith('/users')
* );
* console.log(`User routes: ${userRoutes.length}`);
*
* // Get routes with descriptions
* const documentedRoutes = router.listRoutes().filter(route =>
* route.description
* );
* documentedRoutes.forEach(route => {
* console.log(`${route.method} ${route.path}: ${route.description}`);
* });
* ```
*/
listRoutes(method) {
if (method) return this.#routes.filter((p) => p.method === method);
return this.#routes;
}
}