You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Feature Request: Ability to list routes with method + middleware, one entry per HTTP method #6481
Feature Request: Ability to list routes with method + middleware, one entry per HTTP method
Use case & goal
In medium to large applications, it's often difficult to inspect the full list of declared routes, especially when dealing with multiple routers, dynamic parameters, and complex middleware stacks.
This feature would help developers:
Debug route registration and conflicts.
Generate API documentation.
Inspect which middleware are applied to each route.
Currently, developers have to dig into app._router.stack, which is undocumented and subject to change, or use third-party packages like express-list-endpoints. A native utility would make this more reliable and portable.
I quickly (and not necessarily in an elegant way) adapted the script from the excellent package express-list-endpoints package, and it seems that this version work with Express 5. However, I’m not sure if the regular expressions are fully compatible with the new path-to-regexp@8.
The script currently returns a string list of all methods, routes, and their associated middlewares — not yet an array of objects, which would be more structured and convenient to work with.
This kind of functionality could be a great built-in feature in Express 5, helping developers map out routes and even build documentation or Swagger-like outputs more easily.
/*** a quick modification (and not necessarily in an elegant way) from https://www.npmjs.com/package/express-list-endpoints ***/
/**
* @typedef {Object} Route
* @property {Object} methods
* @property {string | string[]} path
* @property {any[]} stack
*
* @typedef {Object} Endpoint
* @property {string} path Path name
* @property {string[]} methods Methods handled
* @property {string[]} middlewares Mounted middlewares
*/
const regExpToParseExpressPathRegExp = /^\/\^\\?\/?(?:(:?[\w\\.-]*(?:\\\/:?[\w\\.-]*)*)|(\(\?:\\?\/?\([^)]+\)\)))\\\/.*/
const regExpToReplaceExpressPathRegExpParams = /\(\?:\\?\/?\([^)]+\)\)/
const regexpExpressParamRegexp = /\(\?:\\?\\?\/?\([^)]+\)\)/g
const regexpExpressPathParamRegexp = /(:[^)]+)\([^)]+\)/g
const EXPRESS_ROOT_PATH_REGEXP_VALUE = '/^\\/?(?=\\/|$)/i'
const STACK_ITEM_VALID_NAMES = [
'router',
'bound dispatch',
'mounted_app',
'<anonymous>'
]
/**
* Returns all the verbs detected for the passed route
* @param {Route} route
*/
const getRouteMethods = function (route) {
let methods = Object.keys(route.methods);
methods = methods.filter((method) => method !== "_all");
methods = methods.map((method) => method.toUpperCase());
return methods[0] ? methods : ["ALL"];
};
/**
* Returns the names (or anonymous) of all the middlewares attached to the
* passed route
* @param {Route} route
* @returns {string[]}
*/
const getRouteMiddlewares = function (route) {
return route.stack.map((item) => {
return item.name || item.handle.name || 'anonymous'
})
}
/**
* Returns true if found regexp related with express params
* @param {string} expressPathRegExp
* @returns {boolean}
*/
const hasParams = function (expressPathRegExp) {
return regexpExpressParamRegexp.test(expressPathRegExp)
}
/**
* @param {Route} route Express route object to be parsed
* @param {string} basePath The basePath the route is on
* @return {Endpoint[]} Endpoints info
*/
const parseExpressRoute = function (route, basePath) {
const paths = []
if (Array.isArray(route.path)) {
paths.push(...route.path)
} else {
paths.push(route.path)
}
/** @type {Endpoint[]} */
const endpoints = paths.map((path) => {
const completePath = basePath && path === '/'
? basePath
: `${basePath}${path}`
/** @type {Endpoint} */
const endpoint = {
path: completePath.replace(regexpExpressPathParamRegexp, '$1'),
methods: getRouteMethods(route),
middlewares: getRouteMiddlewares(route)
}
return endpoint
})
return endpoints
}
/**
* @param {RegExp} expressPathRegExp
* @param {any[]} params
* @returns {string}
*/
const parseExpressPath = function (expressPathRegExp, params) {
let parsedRegExp = expressPathRegExp.toString()
let expressPathRegExpExec = regExpToParseExpressPathRegExp.exec(parsedRegExp)
let paramIndex = 0
while (hasParams(parsedRegExp)) {
const paramName = params[paramIndex].name
const paramId = `:${paramName}`
parsedRegExp = parsedRegExp
.replace(regExpToReplaceExpressPathRegExpParams, (str) => {
// Express >= 4.20.0 uses a different RegExp for parameters: it
// captures the slash as part of the parameter. We need to check
// for this case and add the slash to the value that will replace
// the parameter in the path.
if (str.startsWith('(?:\\/')) {
return `\\/${paramId}`
}
return paramId
})
paramIndex++
}
if (parsedRegExp !== expressPathRegExp.toString()) {
expressPathRegExpExec = regExpToParseExpressPathRegExp.exec(parsedRegExp)
}
const parsedPath = expressPathRegExpExec[1].replace(/\\\//g, '/')
return parsedPath
}
/**
* @param {import('express').Express | import('express').Router | any} app
* @param {string} [basePath]
* @param {Endpoint[]} [endpoints]
* @returns {Endpoint[]}
*/
const parseEndpoints = function (app, basePath, endpoints) {
const stack = app.stack || (app._router && app._router.stack)
endpoints = endpoints || []
basePath = basePath || ''
if (!stack) {
if (endpoints.length) {
endpoints = addEndpoints(endpoints, [{
path: basePath,
methods: [],
middlewares: []
}])
}
} else {
endpoints = parseStack(stack, basePath, endpoints)
}
return endpoints
}
/**
* Ensures the path of the new endpoints isn't yet in the array.
* If the path is already in the array merges the endpoints with the existing
* one, if not, it adds them to the array.
*
* @param {Endpoint[]} currentEndpoints Array of current endpoints
* @param {Endpoint[]} endpointsToAdd New endpoints to be added to the array
* @returns {Endpoint[]} Updated endpoints array
*/
const addEndpoints = function (currentEndpoints, endpointsToAdd) {
endpointsToAdd.forEach((endpoint) => {
currentEndpoints.push({
"#": currentEndpoints.length + 1,
method: endpoint.methods[0],
path: endpoint.path,
middlewares: endpoint.middlewares
});
});
return currentEndpoints;
};
/**
* @param {any[]} stack
* @param {string} basePath
* @param {Endpoint[]} endpoints
* @returns {Endpoint[]}
*/
const parseStack = function (stack, basePath, endpoints) {
stack.forEach((stackItem) => {
if (stackItem.route) {
const newEndpoints = parseExpressRoute(stackItem.route, basePath);
endpoints = addEndpoints(endpoints, newEndpoints);
} else {
const isExpressPathRegexp = regExpToParseExpressPathRegExp.test(stackItem.regexp);
let newBasePath = basePath;
if (isExpressPathRegexp) {
const parsedPath = parseExpressPath(stackItem.regexp, stackItem.keys);
newBasePath += `/${parsedPath}`;
} else if (!stackItem.path && stackItem.regexp && stackItem.regexp.toString() !== EXPRESS_ROOT_PATH_REGEXP_VALUE) {
const regExpPath = ` RegExp(${stackItem.regexp}) `;
newBasePath += `/${regExpPath}`;
}
if (STACK_ITEM_VALID_NAMES.includes(stackItem.name)) endpoints = parseEndpoints(stackItem.handle, newBasePath, endpoints);
else {
if (!useMiddlewares[newBasePath]) useMiddlewares[newBasePath] = [];
useMiddlewares[newBasePath].push(stackItem.name);
}
}
});
return endpoints;
};
/**
* Returns an array of strings with all the detected endpoints
* @param {import('express').Express | import('express').Router | any} app The express/router instance to get the endpoints from
* @returns {Endpoint[]}
*/
let useMiddlewares = {};
const expressListEndpoints = function (app) {
const endpoints = parseEndpoints(app);
let ret = [];
endpoints.forEach((endpoint, i) => {
for (const path in useMiddlewares) {
if (path && endpoint.path.startsWith(path)) endpoint.middlewares.splice(0, 0, ...(useMiddlewares[path] || []));
}
ret.push(`${String(i + 1).padEnd(4, ' ')} - ${(endpoint?.method || "").padEnd(6, ' ')} - ${endpoint.path.padEnd(30, ' ')} - ${endpoint.middlewares.join(", ")}`);
});
return ret;
};
export default expressListEndpoints
It seems that many developers have tried to achieve the same goal using different approaches (stackoverflow).
All the solutions rely on some kind of reverse engineering to figure out how to navigate the Express stack. That’s why I believe the best approach would be to have a built-in function — to avoid misunderstandings and misinterpretations of how the Express stack works.
Reacted by AcePeaX, qq0, infuzz, Ilya and shravan || श्रvan
With the help of my AI friend 😉, I completely rewrote the listRoutes function, and overall everything is running really well! The only challenge left is detecting sub-router routes. I've explored many different approaches, but it seems very difficult — if not impossible — to retrieve the sub-route paths passed with statements like app.use("/subpath", router). It looks like these paths simply aren't available in the stack.
Any insights, tips, or ideas would be truly appreciated! It's an exciting problem to work on, and it really highlights how valuable it would be for Express 5 to offer a built-in way to list all registered routes, instead of having to dive deep and reverse-engineer the framework.
Thanks a lot in advance for any help!
/**
* Extracts all routes from an Express 5.1.0 app or Router.
* If options.format = true, returns an array of formatted strings;
* otherwise returns an array of { method, route, middlewares }.
*
* @param {import('express').Application|import('express').Router} appOrRouter
* @param {{ format?: boolean }} options
* @returns {Array<{ method: string, route: string, middlewares: string[] }> | string[]}
*/
listRoutes(appOrRouter, options = {}) {
const routes = [];
const root = appOrRouter._router || appOrRouter;
const stack = root.stack || [];
// Try to recover a real function name, even from anonymous/factory fns
function getMiddlewareName(fn) {
if (fn.name) return fn.name;
const m = /^function\s*([\w$]+)/.exec(fn.toString());
return m ? m[1] : "<anonymous>";
}
// Join two URL segments, ensuring exactly one "/" between them
function joinPaths(a, b) {
if (!b) return a;
const A = a.endsWith("/") ? a.slice(0, -1) : a;
const B = b.startsWith("/") ? b : "/" + b;
return A + B;
}
// Given any Layer, figure out its "mount path" portion:
// • layer.path (Express 5 sets this on .use)
// • layer.route.path (for route layers)
// • fallback: parse layer.regexp.toString()
function getLayerPath(layer) {
if (layer.path != null) {
return layer.path === "/" ? "" : layer.path;
}
if (layer.route && layer.route.path != null) {
return layer.route.path === "/" ? "" : layer.route.path;
}
if (layer.regexp) {
// fast_slash means "/"
if (layer.regexp.fast_slash) return "";
// strip off the /^ \/foo \/?(?=\/|$) /i wrapper
let str = layer.regexp
.toString()
.replace(/^\/\^/, "")
.replace(/\\\/\?\(\?=\\\/\|\$\)\/i$/, "")
.replace(/\\\//g, "/")
.replace(/\\\./g, ".");
// replace capture groups with :param
const keys = (layer.keys || []).map((k) => `:${k.name}`);
str = str.replace(/\((?:\?:)?[^\)]+\)/g, () => keys.shift() || "");
return str.startsWith("/") ? str : "/" + str;
}
return "";
}
/**
* Recursively walk a stack of Layers, accumulating:
* - `prefix` the URL so far ("/v7", then "/v7/webhooks", etc.)
* - `parentMW` the array of middleware names mounted above this point
*/
function traverse(stack, prefix = "", parentMW = []) {
stack.forEach((layer, idx) => {
// ─── Case 1: a direct route (app.get / router.post / etc) ───
if (layer.route) {
const routePath = layer.route.path || "";
const fullPath = joinPaths(prefix, routePath);
const methods = Object.keys(layer.route.methods).map((m) => m.toUpperCase());
const routeMW = layer.route.stack.map((l) => getMiddlewareName(l.handle));
const allMW = parentMW.concat(routeMW);
methods.forEach((method) => {
routes.push({ method, route: fullPath, middlewares: allMW });
});
// ─── Case 2: a mounted router or .use(path, fn, ...) block ───
} else if (layer.handle && layer.handle.stack) {
const mountPath = getLayerPath(layer);
const fullPref = joinPaths(prefix, mountPath);
// gather any plain middleware functions mounted at this same mountPath
const mountMW = stack
.slice(0, idx)
.filter(
(prev) =>
typeof prev.handle === "function" &&
!prev.handle.stack && // not a router
getLayerPath(prev) === mountPath // same mount path
)
.map((prev) => getMiddlewareName(prev.handle));
// recurse into child stack, carrying forward middleware names
traverse(layer.handle.stack, fullPref, parentMW.concat(mountMW));
}
// else: a logger, error handler, or global middleware—skip it
});
}
traverse(stack);
// If asked for formatted strings, pad each column to align the dashes
if (options.format) {
const idxW = String(routes.length).length;
const mthW = Math.max(...routes.map((r) => r.method.length), 6);
const rteW = Math.max(...routes.map((r) => r.route.length), 5);
return routes.map((r, i) => {
const idx = String(i + 1).padEnd(idxW, " ");
const mtd = r.method.padEnd(mthW, " ");
const rte = r.route.padEnd(rteW, " ");
const mws = r.middlewares.join(", ");
return `${idx} - ${mtd} - ${rte} - ${mws}`;
});
}
return routes;
}
Reacted by infuzz, Joshua Hintze and marcellino ornelas
I would love for this to become part of the framework as well. In 4.X that I was using I too used the app._router._stack approach but just switching to 5.X those internal variables are gone which makes sense. Having a framework capability would be great. We use this for auditing purposes to see if any routes we have created have never been called.
Reacted by infuzz, Nuno, eviltik and Eron Tancioco
I’m following up on this ticket to gather some feedback from the maintainers and contributors.
Since Express 5 no longer exposes the internal routing structure (such as app._router.stack), it's no longer possible to introspect routes reliably — and external modules that relied on it no longer work either. This makes it difficult to list defined routes (methods, paths, middleware) for purposes like debugging, documentation, or tooling.
Given this situation, I’m wondering what you think about the idea of adding a public API like app.getRoutes() or express.listRoutes(app) to expose this information in a supported and stable way.
Does this seem compatible with the project's philosophy?
Are there concerns or past discussions I might have missed?
Thanks in advance for your thoughts!
Reacted by Mark Parnwell, Michael Wieland, Eron Tancioco, Firdaus Asrar and docmason-dot-co
@infuzz - I don't have much history. I'm not sure if you are a maintainer but I would really love a public api to get this information. How I use this information is I create an audit record of all the endpoints that are registered and store that in a redis cache. I increment when endpoints are called in the cache. This way I can see if there are dead endpoints that are never called and no longer needed.
@sheplu@infuzz Sorry for the ping but any progress on getting something like this implemented on your roadmap 👀
The solution provided above did an ok job and can get the job done for now but had some flaws and wondering if something like this is going to be supported long term.
However we have some middleware attached to our API that returns a 401 before hitting any routes. The issue is that on routes that have params (example: /api/some/:param/route), they always get reported in the originalUrl form (example: /api/some/myValue/route) because the middleware is hit before req.route?.path is set. Regular status codes always get the right value. Soo my goal is to use this logic to pragmatically fetch the route it was supposed to hit to I can report the correct express route back to Prometheus instead of the originalUrl.
Activity
destroyer22719 commentedon Apr 24, 2025
you kind of can already do something similar but...
#3308 (comment)
infuzz commentedon Apr 24, 2025
I quickly (and not necessarily in an elegant way) adapted the script from the excellent package express-list-endpoints package, and it seems that this version work with Express 5. However, I’m not sure if the regular expressions are fully compatible with the new path-to-regexp@8.
The script currently returns a string list of all methods, routes, and their associated middlewares — not yet an array of objects, which would be more structured and convenient to work with.
This kind of functionality could be a great built-in feature in Express 5, helping developers map out routes and even build documentation or Swagger-like outputs more easily.
It seems that many developers have tried to achieve the same goal using different approaches (stackoverflow).
All the solutions rely on some kind of reverse engineering to figure out how to navigate the Express stack. That’s why I believe the best approach would be to have a built-in function — to avoid misunderstandings and misinterpretations of how the Express stack works.
infuzz commentedon Apr 28, 2025
With the help of my AI friend 😉, I completely rewrote the listRoutes function, and overall everything is running really well! The only challenge left is detecting sub-router routes. I've explored many different approaches, but it seems very difficult — if not impossible — to retrieve the sub-route paths passed with statements like app.use("/subpath", router). It looks like these paths simply aren't available in the stack.
Any insights, tips, or ideas would be truly appreciated! It's an exciting problem to work on, and it really highlights how valuable it would be for Express 5 to offer a built-in way to list all registered routes, instead of having to dive deep and reverse-engineer the framework.
Thanks a lot in advance for any help!
JoshuaHintze commentedon May 5, 2025
I would love for this to become part of the framework as well. In 4.X that I was using I too used the app._router._stack approach but just switching to 5.X those internal variables are gone which makes sense. Having a framework capability would be great. We use this for auditing purposes to see if any routes we have created have never been called.
infuzz commentedon Jun 24, 2025
Hi everyone 👋
I’m following up on this ticket to gather some feedback from the maintainers and contributors.
Since Express 5 no longer exposes the internal routing structure (such as app._router.stack), it's no longer possible to introspect routes reliably — and external modules that relied on it no longer work either. This makes it difficult to list defined routes (methods, paths, middleware) for purposes like debugging, documentation, or tooling.
Given this situation, I’m wondering what you think about the idea of adding a public API like app.getRoutes() or express.listRoutes(app) to expose this information in a supported and stable way.
Does this seem compatible with the project's philosophy?
Are there concerns or past discussions I might have missed?
Thanks in advance for your thoughts!
JoshuaHintze commentedon Jun 25, 2025
@infuzz - I don't have much history. I'm not sure if you are a maintainer but I would really love a public api to get this information. How I use this information is I create an audit record of all the endpoints that are registered and store that in a redis cache. I increment when endpoints are called in the cache. This way I can see if there are dead endpoints that are never called and no longer needed.
OnlyWick commentedon Jul 24, 2025
try
app.router.stackinexpress 5.xmarcellino-ornelas commentedon Sep 15, 2025
@sheplu @infuzz Sorry for the ping but any progress on getting something like this implemented on your roadmap 👀
The solution provided above did an ok job and can get the job done for now but had some flaws and wondering if something like this is going to be supported long term.
For my use case, Im using
express-prom-bundleto report metrics to prometheus. We are using https://github.com/jochen-schweizer/express-prom-bundle?tab=readme-ov-file#example-3-return-express-route-definition soo that it reports the routes we define in express soo parameterized routes get reported as the same route.However we have some middleware attached to our API that returns a 401 before hitting any routes. The issue is that on routes that have params (example:
/api/some/:param/route), they always get reported in the originalUrl form (example:/api/some/myValue/route) because the middleware is hit beforereq.route?.pathis set. Regular status codes always get the right value. Soo my goal is to use this logic to pragmatically fetch the route it was supposed to hit to I can report the correct express route back to Prometheus instead of the originalUrl.pw-64 commentedon Oct 27, 2025
fyi pillarjs/router#174 is ready to merge so hopefully we'll see a proper and maintained solution soon!
JoshuaHintze commentedon Oct 28, 2025
Looking forward to it @pw-64
krzysdz commentedon Oct 22, 2026
@bjohansebas I think that this should be kept open until #7531 is merged.