Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,47 @@ router.param('user_id', function (req, res, next, id) {
})
```

### route.getRoutes()

Copy link
Copy Markdown
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think listRoutes() is reasonable.


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())
// [
// {
// key: '/admin/',
// methods: ['GET'],
// keys: [],
// options: { strict: true, caseSensitive: false },
// },
// {
// key: '/:id',
// methods: ['ALL'],
// keys: [{ name: 'id', type: "param" }],
// options: { strict: true, caseSensitive: true },
// }
// ]
```

### router.route(path)

Creates an instance of a single `Route` for the given `path`.
Expand Down
107 changes: 107 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const Layer = 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 +442,26 @@ 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 routes = []
const stack = this.stack

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

collectRoutes(stack, '', routes, options)

return routes
}
Comment on lines +452 to +461

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Small formatting nitpick (not required, just personal preference):

Suggested change
Router.prototype.getRoutes = function getRoutes () {
const stack = this.stack
const options = {
strict: this.strict,
caseSensitive: this.caseSensitive
}
return collectRoutes(stack, options)
}
Router.prototype.getRoutes = function getRoutes () {
return collectRoutes(this.stack, {
strict: this.strict,
caseSensitive: this.caseSensitive
})
}


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

/**
* Add a route to the map with the given path and methods.
* @param {Map} routeMap
* @param {string} path
* @param {Array} methods
* @private
*/
function addRouteToMap (routeMap, path, methods, options) {
const { keys } = pathRegexp.pathToRegexp(path)

routeMap.push(
{
path,
methods: [...methods],
keys,
options: { strict: options.strict, caseSensitive: options.caseSensitive }
}
)
}

/**
* Normalize a path by removing trailing slashes.
* @param {string} path
* @return {string} normalized path
* @private
*/
function normalizePath (path) {
if (typeof path !== 'string') {
return path
}

if (path.endsWith('/') && path.length > 1) {
return path.slice(0, -1)
}

return path
}

/**
* Collect routes from a router stack recursively.
*
* @param {Array} stack - The router stack to collect routes from
* @param {string} prefix - The path prefix to prepend to routes
* @param {Map} routeMap - The map to store collected routes
* @private
*/
function collectRoutes (stack, prefix, routeMap, options) {
for (const layer of stack) {
// for routes without a .use
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 fullPath = prefix === '/' ? pathPattern : normalizePath(prefix) + pathPattern
addRouteToMap(routeMap, fullPath, methods, options)
}
} else {
const fullPath = prefix === '/' ? layer.pathPatterns : prefix + layer.pathPatterns
addRouteToMap(routeMap, fullPath, methods, options)
}
}

// for layers with a .use (mounted routers)
if (layer.pathPatterns && layer.handle && layer.handle.stack && !layer.route) {

Copy link
Copy Markdown
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
Copy Markdown
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.

@blakeembrey blakeembrey Jul 29, 2025

Copy link
Copy Markdown
Member

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
Copy Markdown
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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agreed here, lets just simplify this down into the bare minimum api before merging this PR. Then we can take on nesting routers and deeply iterating the stack more directly later if we deem it worth it. We want to unstuck this progress, and I think these details is what I got hung up on last time leading to my original blocking review. Better to land what we can for sure agree on adds value and move this forward.

if (Array.isArray(layer.pathPatterns)) {
for (const pathPattern of layer.pathPatterns) {
const pathPrefix = prefix === '/' ? normalizePath(pathPattern) : prefix + normalizePath(pathPattern)

collectRoutes(layer.handle.stack, pathPrefix, routeMap, {
strict: layer.handle.strict,
caseSensitive: layer.handle.caseSensitive
})
}
} else {
const pathPrefix = prefix === '/' ? normalizePath(layer.pathPatterns) : prefix + normalizePath(layer.pathPatterns)

collectRoutes(layer.handle.stack, pathPrefix, routeMap, {
strict: layer.handle.strict,
caseSensitive: layer.handle.caseSensitive
})
}
}
}
}

/**
* Generate a callback that will make an OPTIONS response.
*
Expand Down
3 changes: 3 additions & 0 deletions lib/layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ 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.pathPatterns = path

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The naming of this implies to me two things:

  1. it is always a list (which it is not, it can be a few things)
  2. That it is related somehow to URLPattern (which it is not, but we would want to support someday so avoiding naming confusion now seems good)

Maybe a nitpick the naming would be we could call it rawPath or something similar to that so that it is clear this is "what the user passed in"?

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

function matcher (_path) {
Expand Down
201 changes: 201 additions & 0 deletions test/getRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
const { it, describe } = require('mocha')
const Router = require('..')
const utils = require('./support/utils')

const assert = utils.assert

describe('getRoutes', function () {
it('should return an empty array when no routes are registered', function () {
const router = new Router()

assert.deepStrictEqual(router.getRoutes(), [])
})

it('should return route information for various route types (strings, arrays, and parameterized paths)', function () {
const router = new Router()

router.all('/', noop)
router.route('/test2/')
router.route('/test/').get(noop)
// With regex patterns path-to-regexp fail
// router.all(/^\/[a-z]oo$/, noop)
router.get(['/foo', '/bar'], noop)
router.post('/:id/setting/:thing', noop)

assert.deepStrictEqual(router.getRoutes(),
[
{ path: '/', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test2/', methods: [], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
// { path: '/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/foo', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/bar', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/:id/setting/:thing', methods: ['POST'], keys: [{ name: 'id', type: 'param' }, { name: 'thing', type: 'param' }], options: { strict: undefined, caseSensitive: undefined } }
])
})

it('should track multiple registrations of the same route with different HTTP methods', function () {
const router = new Router()

router.post(['/test', '/test2'], noop)

for (let i = 0; i < 2; i++) {
router.get(['/test', '/test3'], noop)
}

router.put('/test3', noop)

assert.deepStrictEqual(router.getRoutes(), [
{ path: '/test', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test2', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test3', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test3', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test3', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } }
])
})

it('should properly handle nested routers and multiple mount points', function () {
const router = new Router()
const inner = new Router()
router.post('/test', noop)

for (let i = 0; i < 2; i++) {
router.get('/test', noop)
}

for (let i = 0; i < 2; i++) {
inner.get('/test', noop)
}

router.use(['/test/', '/test2', '/test3'], inner)
router.use('/test4/', inner)
router.route('/test5').get(noop).post(noop)

assert.deepStrictEqual(router.getRoutes(), [
{ path: '/test', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test2/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test2/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test3/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test3/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test4/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test4/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/test5', methods: ['GET', 'POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } }
])
})

it('should correctly flatten deeply nested router hierarchies with multiple levels', function () {
const router = new Router()
const inner = new Router()
const subinner = new Router()

subinner.put('/t5', noop)
// subinner.all(/^\/[a-z]oo$/, noop)
subinner.use(noop)

inner.use('/t3', subinner)
inner.all('/t4', noop)
inner.get('/', noop)
inner.use(noop)

router.use('/t2', inner)
router.use(['/t5', '/t7'], inner)

router.use(noop)
router.use('/test1', noop)

assert.deepStrictEqual(router.getRoutes(), [
{ path: '/t2/t3/t5', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
// { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [],options: { strict: undefined, caseSensitive: undefined } },
{ path: '/t2/t4', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/t2/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/t5/t3/t5', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
// { path: '/t5/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [],options: { strict: undefined, caseSensitive: undefined } },
{ path: '/t5/t4', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/t5/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/t7/t3/t5', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
// { path: '/t7/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [],options: { strict: undefined, caseSensitive: undefined } },
{ path: '/t7/t4', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } },
{ path: '/t7/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }
])
})

it('should avoid double slashes when mounting routers at root path', function () {
const router = new Router()
const subRouter = new Router()

subRouter.get('/api', () => {})
router.use('/', subRouter)

const routes = router.getRoutes()

assert.deepStrictEqual(routes, [
{ path: '/api', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }
])
})

it('should preserve router configuration options from parent to child routers', function () {
const router = new Router({ strict: true, caseSensitive: true })
const inner = new Router({ strict: true, caseSensitive: false })
const subinner = new Router({ strict: false, caseSensitive: false })

subinner.put('/t5', noop)
subinner.use(noop)

inner.use('/t3', subinner)
inner.all('/t4', noop)
inner.get('/', noop)
inner.use(noop)

router.use('/t2', inner)
router.use(['/t5', '/t7'], inner)

router.use(noop)
router.get('/test', noop)

assert.deepStrictEqual(router.getRoutes(), [
{ path: '/t2/t3/t5', methods: ['PUT'], keys: [], options: { strict: false, caseSensitive: false } },
{ path: '/t2/t4', methods: ['_ALL'], keys: [], options: { strict: true, caseSensitive: false } },
{ path: '/t2/', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: false } },
{ path: '/t5/t3/t5', methods: ['PUT'], keys: [], options: { strict: false, caseSensitive: false } },
{ path: '/t5/t4', methods: ['_ALL'], keys: [], options: { strict: true, caseSensitive: false } },
{ path: '/t5/', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: false } },
{ path: '/t7/t3/t5', methods: ['PUT'], keys: [], options: { strict: false, caseSensitive: false } },
{ path: '/t7/t4', methods: ['_ALL'], keys: [], options: { strict: true, caseSensitive: false } },
{ path: '/t7/', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: false } },
{ path: '/test', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: true } }
])
})

it('should handle multiple routers with different configuration options mounted at the same path', function () {
const router = new Router({ strict: true, caseSensitive: true })
const inner = new Router({ strict: true, caseSensitive: false })
const otherInner = new Router({ strict: true, caseSensitive: true })
const otherInner2 = new Router({ strict: true, caseSensitive: false })

otherInner2.put('/:t5', noop)
otherInner2.get('/:t6', noop)

otherInner.put('/:t5', noop)
otherInner.post('/:t6', noop)

inner.use('/t2', otherInner)
inner.use('/t2', otherInner2)

router.use(inner)

assert.deepStrictEqual(router.getRoutes(), [
{ path: '/t2/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], options: { strict: true, caseSensitive: true } },
{ path: '/t2/:t6', methods: ['POST'], keys: [{ name: 't6', type: 'param' }], options: { strict: true, caseSensitive: true } },
{ path: '/t2/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], options: { strict: true, caseSensitive: false } },
{ path: '/t2/:t6', methods: ['GET'], keys: [{ name: 't6', type: 'param' }], options: { strict: true, caseSensitive: false } }
])
})
})

function noop () {}