Skip to content

Add util to print full paths of routes and methods registered across the application #7268

@sheytoonch

Description

@sheytoonch

I am asking someone to implement the feature.

I request a feature to print all available routes registered in very easy way to handle. Something like:
registeredRoutes()
And it would return for example an array of routes:
[{method: 'POST', path: '/api/v1/route1/:param1'},...]

I've seen many people online build their own solutions to retrieve all available routes, but every version of express changes a bit its structure and it gets harder to do so without very nasty looking tricks. I did mine too, but now I just don't know how to do it.

My current solution allows me to just copy the route and use it in postman, also helps with visibility, as I can just print all routes and see if I can find useful endpoint.

2026-05-19T09:29:45.793Z APP INFO, [App] Registered routes: 5
2026-05-19T09:29:45.794Z APP INFO, [Available Route] GET /api-docs
2026-05-19T09:29:45.794Z APP INFO, [Available Route] GET /api-docs.json
2026-05-19T09:29:45.794Z APP INFO, [Available Route] POST /api/data-integration/service-profile
2026-05-19T09:29:45.794Z APP INFO, [Available Route] GET /health
2026-05-19T09:29:45.794Z APP INFO, [Available Route] GET /sandbox/1

Right now I have a function called: showAppRoutes(app);

Its code looks like this:

import type { Express } from 'express';
import { logger } from '../infra/logger';
import { appRoutes, apiRoutes } from '../routes';

type RouteInfo = { method: string; path: string };

export const showAppRoutes = (app: Express): void => {
    const routes: RouteInfo[] = [];

    let stack: any[] = [];
    const appAny = app as any;

    if (!appAny._router && appAny.router) {
        appAny._router = appAny.router.router;
    }

    stack = appAny?._router?.stack || [];

    if (stack.length === 0 && appAny.router && appAny.router.stack) {
        stack = appAny.router.stack;
    }

    // Create a map of router handles to their mount paths
    const routerMountPaths: Map<any, string> = new Map([
        [appRoutes, '/'],
        [apiRoutes, '/api'],
    ]);

    const extractRoutes = (layers: any[], prefix = '') => {
        for (const layer of layers) {
            if (layer.route) {
                let path = layer.route.path || '';

                // Build the full path, avoiding double slashes
                if (prefix === '/' || prefix === '') {
                    path = (prefix === '/' ? '' : prefix) + path;
                } else {
                    path = prefix + path;
                }

                const methods = Object.keys(layer.route.methods || {});
                methods.forEach(method => {
                    routes.push({
                        method: method.toUpperCase(),
                        path: path || '/',
                    });
                });
            } else if (layer.name === 'router' && layer.handle?.stack) {
                let routePrefix = '';

                // Check if we know the mount path for this router
                if (routerMountPaths.has(layer.handle)) {
                    routePrefix = routerMountPaths.get(layer.handle) || '';
                }
                // Otherwise check for __ROUTE_PREFIX__ metadata
                else if ((layer.handle as any).__ROUTE_PREFIX__) {
                    routePrefix = (layer.handle as any).__ROUTE_PREFIX__;
                }

                extractRoutes(layer.handle.stack, prefix + routePrefix);
            }
        }
    };

    extractRoutes(stack);

    const seen = new Set<string>();
    const unique = routes
        .filter(r => {
            const key = `${r.method} ${r.path}`;
            if (seen.has(key)) return false;
            seen.add(key);
            return true;
        })
        .sort((a, b) => {
            const pathCompare = a.path.localeCompare(b.path);
            return pathCompare !== 0 ? pathCompare : a.method.localeCompare(b.method);
        });

    // Add Swagger UI endpoints (middleware-based)
    const swaggerRoutes = [
        { method: 'GET', path: '/api-docs' },
        { method: 'GET', path: '/api-docs.json' },
    ];

    const allRoutes = [...unique, ...swaggerRoutes]
        .filter((v, i, a) => a.findIndex(t => t.method === v.method && t.path === v.path) === i)
        .sort((a, b) => {
            const pathCompare = a.path.localeCompare(b.path);
            return pathCompare !== 0 ? pathCompare : a.method.localeCompare(b.method);
        });

    logger.info(`[App] Registered routes: ${allRoutes.length}`);

    const METHOD_WIDTH = 8;
    allRoutes.forEach(r => {
        const methodPadded = r.method.padStart(METHOD_WIDTH, ' ');
        logger.info(`[Available Route] ${methodPadded} ${r.path}`);
    });
};

But I also need to put some variables across my app for it to work:
(router as any).__ROUTE_PREFIX__ = '/data-integration';

My previous code returned exactly the same format, but was cleaner.

import type { Express } from 'express';
import { logger } from '../infra/logger';

type RouteInfo = { method: string; path: string };

// Best-effort extraction of a mounted prefix from a router layer
function getLayerPrefix(layer: any): string {
    // Prefer explicit path if present (Express may set it on some versions)
    if (typeof layer.path === 'string') return layer.path;

    // Fast root router
    if (layer?.regexp?.fast_slash) return '';

    const keys = Array.isArray(layer?.keys) ? layer.keys : [];
    // Try to reconstruct from keys (e.g., /:id)
    if (keys.length) {
        return '/' + keys.map((k: any) => `:${k.name}`).join('/');
    }

    // Fallback: parse from regexp
    const src = String(layer?.regexp || '');
    // Typical: /^\/api\/v1\/users\/?(?=\/|$)/i
    const match = src.match(/^\/\^\\\/(.*)\\\/\?\(\?\=\\\/\|\$\)\/i$/);
    if (match && match[1]) {
        return '/' + match[1].replace(/\\\//g, '/');
    }

    return '';
}

export const showAppRoutes = (app: Express): void => {
    const stack: any[] = (app as any)?._router?.stack || [];
    const routes: RouteInfo[] = [];

    const walk = (layers: any[], prefix = '') => {
        for (const layer of layers) {
            if (layer?.route?.path) {
                const fullPath = prefix + layer.route.path;
                const methods = Object.keys(layer.route.methods || {});
                for (const m of methods) {
                    routes.push({ method: m.toUpperCase(), path: fullPath });
                }
            } else if (layer?.name === 'router' && layer?.handle?.stack) {
                const newPrefix = prefix + getLayerPrefix(layer);
                walk(layer.handle.stack, newPrefix);
            }
        }
    };

    walk(stack);

    // Deduplicate and sort alphabetically by path, then by method
    const seen = new Set<string>();
    const unique = routes.filter(r => {
        const k = `${r.method} ${r.path}`;
        if (seen.has(k)) return false;
        seen.add(k);
        return true;
    }).sort((a, b) => {
        // Primary sort: alphabetical by path
        const pathCompare = a.path.localeCompare(b.path);
        if (pathCompare !== 0) return pathCompare;
        // Secondary sort: by method if paths are the same
        return a.method.localeCompare(b.method);
    });

    logger.info(`[App] Registered routes: ${unique.length}`);

    // Display with right-aligned method padding so last letters align
    const METHOD_WIDTH = 8; // Width for HTTP method area (accommodates DELETE = 6 chars)

    unique.forEach(r => {
        // Right-align the method within the fixed width
        const methodPadded = r.method.padStart(METHOD_WIDTH, ' ');
        logger.info(`[Available Route] ${methodPadded} ${r.path}`);
    });
};

I'd really uppreciate if someone had a look and the will to implement the feature.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions