Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,60 @@ router.param('user_id', function (req, res, next, id) {
})
```

### route.getRoutes()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wont block on this but Id prefer to see route.routes() or route.listRoutes() to avoid intellisense/tab complete conflict when folks type app/router.g in an attempt to type .get


Returns an array of all the routes registered on this route, including
all the methods, key, and the options of instance of router.

```js
const router = new Router({ strict: true, caseSensitive: true })
const admin = new Router({ strict: true, caseSensitive: false })

admin.use((req, res, next) => {
// some middleware for admin routes
next()
})

admin.get('/', (req, res, next) => {
res.end('Hello')
})

router.use("/admin", admin)

router.all('/:id', function (req, res) {
res.end('Hello')
})

console.log(router.getRoutes())
// [
// {
// name: 'router',
// path: '/admin',
// methods: undefined,
// keys: undefined,
// router: [
// {
// name: 'handle',
// path: '/',
// methods: ['GET'],
// keys: undefined,
// router: undefined,
// options: { strict: true, caseSensitive: false, end: true },
// }
// ],
// options: { strict: true, caseSensitive: true, end: false }
// },
// {
// name: 'handle',
// path: '/:id',
// methods: ['_ALL'],
// keys: [{ name: 'id', type: "param" }],
// router: undefined,
// options: { strict: true, caseSensitive: true, end: true }
// }
// ]
```

### router.route(path)

Creates an instance of a single `Route` for the given `path`.
Expand Down
131 changes: 131 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@

const isPromise = require('is-promise')
const Layer = require('./lib/layer')
const { MATCHING_GROUP_REGEXP } = require('./lib/layer')
const { METHODS } = require('node:http')
const parseUrl = require('parseurl')
const Route = require('./lib/route')
const pathRegexp = require('path-to-regexp')
const debug = require('debug')('router')
const deprecate = require('depd')('router')

Expand Down Expand Up @@ -441,6 +443,23 @@ Router.prototype.route = function route (path) {
return route
}

/**
* List all registered routes.
*
* @return {Array} An array of route paths
* @public
*/
Router.prototype.getRoutes = function getRoutes () {
const stack = this.stack

const options = {
strict: this.strict,
caseSensitive: this.caseSensitive
}

return collectRoutes(stack, options)
}

// create Router#VERB functions
methods.concat('all').forEach(function (method) {
Router.prototype[method] = function (path) {
Expand All @@ -450,6 +469,118 @@ methods.concat('all').forEach(function (method) {
}
})

/**
* Collect routes from a router stack recursively.
*
* @param {Array} stack - The router stack to collect routes from
* @param {object} options - The router options
* @private
*/
function collectRoutes (stack, options) {
const routes = []

for (const layer of stack) {
// route layer (has methods)
if (layer.pathPatterns && layer.route) {
const methods = Object.keys(layer.route.methods).map((method) => method.toUpperCase())

if (Array.isArray(layer.pathPatterns)) {
for (const pathPattern of layer.pathPatterns) {
const keys = extractPatternKeys(pathPattern)

routes.push({
name: layer.name,
path: pathPattern,
keys,
methods,
router: undefined,
options: { ...options, end: layer.end }
})
}
} else {
const keys = extractPatternKeys(layer.pathPatterns)

routes.push({
name: layer.name,
path: layer.pathPatterns,
keys,
methods,
router: undefined,
options: { ...options, end: layer.end }
})
}
}

// mounted router (use)
if (layer.pathPatterns && layer.handle && layer.handle.stack && !layer.route) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For .use, could it make sense to allow consumers to iterate recursively themselves? It's something that could be added in a follow up, but the MVP could be simply { path, method, router }. Every field could also be optional, I guess, since path: undefined with .use(fn), method: undefined when all is used, and router: undefined when no nested router.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path will never be undefined .use always sets the path to '/' when no path is explicitly provided in the arguments.

Copy link
Member

@blakeembrey blakeembrey Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the kind of thing that should be nailed down for the API, but understood. It probably is reasonable to keep it as / and the object was intended to be hypothetical.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g. why / vs ""? Does one make it harder for consumers than the other? How do these interact with internal routing behaviors that aren't being exposed in this API? How much can move these expectations/behaviors to be static instead of magic (e.g. removing the trailing / is the one that comes to mind, people need to know how the package works internally).

if (Array.isArray(layer.pathPatterns)) {
for (const pathPattern of layer.pathPatterns) {
const inner = collectRoutes(
layer.handle.stack,
{ strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive }
)
const keys = extractPatternKeys(pathPattern)

routes.push({
name: layer.name,
path: pathPattern,
keys,
methods: undefined,
router: inner.length ? inner : undefined,
options: { ...options, end: layer.end }
})
}
} else {
const inner = collectRoutes(
layer.handle.stack,
{ strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive }
)
const keys = extractPatternKeys(layer.pathPatterns)

routes.push({
name: layer.name,
path: layer.pathPatterns,
keys,
methods: undefined,
router: inner.length ? inner : undefined,
options: { ...options, end: layer.end }
})
}
}
}

return routes
}

/**
* Extracts parameter/key descriptors from a route pattern.
*
* @param {string|RegExp} pattern - Route pattern to analyze (path string or RegExp).
* @returns {Array<Object>|undefined} Array of key descriptor objects (each with at least a `name` property), or `undefined` if none found.
* @private
*/
function extractPatternKeys (pattern) {
if (pattern instanceof RegExp) {
const keys = []
let name = 0
let m
// eslint-disable-next-line no-cond-assign
while (m = MATCHING_GROUP_REGEXP.exec(pattern.source)) {
keys.push({ name: m[1] || name++ })
}

return keys.length > 0 ? keys : undefined
}

const pathKeys = pathRegexp.pathToRegexp(String(pattern)).keys

if (pathKeys && pathKeys.length > 0) {
return pathKeys
}

return undefined
}

/**
* Generate a callback that will make an OPTIONS response.
*
Expand Down
5 changes: 5 additions & 0 deletions lib/layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g
*/

module.exports = Layer
module.exports.MATCHING_GROUP_REGEXP = MATCHING_GROUP_REGEXP

function Layer (path, options, fn) {
if (!(this instanceof Layer)) {
Expand All @@ -43,7 +44,11 @@ function Layer (path, options, fn) {
this.keys = []
this.name = fn.name || '<anonymous>'
this.params = undefined
// path is determinate in runtime execution
this.path = undefined
this.end = opts.end

this.pathPatterns = path
this.slash = path === '/' && opts.end === false

function matcher (_path) {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
},
"scripts": {
"lint": "standard",
"test": "mocha --reporter spec --bail --check-leaks test/",
"test:debug": "mocha --reporter spec --bail --check-leaks test/ --inspect --inspect-brk",
"test": "mocha --reporter spec --check-leaks test/",
"test:debug": "mocha --reporter spec --check-leaks test/ --inspect --inspect-brk",
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
"test-cov": "nyc --reporter=text npm test",
"version": "node scripts/version-history.js && git add HISTORY.md"
Expand Down
Loading