Skip to content

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

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions