express icon indicating copy to clipboard operation
express copied to clipboard

List All Routes in express app

Open bryanCoteChang opened this issue 6 years ago • 29 comments

Hi! I've been fighting this for a bit, but I'm trying to gather a list of all of the endpoints in our express applications. I've tried 'express-list-router' and the code below, but none give the full paths of the endpoint and the allowed methods to use them. I've read the documentation, but can't seem to find the properties/methods to access this information. Thank you in advance for your time and assistance! image

bryanCoteChang avatar May 11 '17 23:05 bryanCoteChang

It is not possible to get a fill list with the built-in router. Since paths are regular expression-based, there is no easy way in which to reverse them into a path.

dougwilson avatar May 11 '17 23:05 dougwilson

Hmmm, I'm auditing a public facing application. Tons of repos and submodules, it would be almost laughable to do it manually. Is my only option then to catch them downstream via: req.method and req.originalUrl?

bryanCoteChang avatar May 12 '17 00:05 bryanCoteChang

**Creative and/or potentially crazy solutions are welcome :)

bryanCoteChang avatar May 12 '17 00:05 bryanCoteChang

There is no general solution that would guarantee you are not missing anything. Is it possible to share the app at all? We could at least see what kind of structure the program is using to see if it's possible at all. What you posted at first is a pretty simplistic method, and if that's not giving what you're looking for, it's hard to really understand why without seeing something.

dougwilson avatar May 12 '17 00:05 dougwilson

O I think I can share my sandbox app! (Way less code anyway.) Two main datapoints I'm trying to capture:

  • All routes directly declared to the app.
  • All routes declared through router(s).

Sandbox:

var expressListRoutes   = require('express-list-routes'),
    express             = require('express'),
    router              = express.Router();

var app = express();
 
app.use('/api/v1', router);

app.get('/', function(req, res){
  res.send('hello world');
});
 
router.route('/user')
  .post(function(test){})
  .get(function(test){})
  .put(function(test){});
    
// expressListRoutes({ prefix: '/api/v1' }, 'API:', router );

var route, routes = [];

app._router.stack.forEach(function(middleware){
    if(middleware.route){ // routes registered directly on the app
        routes.push(middleware.route);
    } else if(middleware.name === 'router'){ // router middleware 
        middleware.handle.stack.forEach(function(handler){
            route = handler.route;
            route && routes.push(route);
        });
    }
});

routes.forEach(function(temp){
	var methods = "";
	for(var method in temp.methods){
		methods += method + ", ";
	}
	console.log(temp.path + ": " + methods);
});

Output: /user: post, get, put, /: get, Expected Output: /api/v1/user: post, get, put, /: get,

bryanCoteChang avatar May 12 '17 00:05 bryanCoteChang

Here is a very, very hacky way for that app. I can't stress how hacky this is, but here it is:

function print (path, layer) {
  if (layer.route) {
    layer.route.stack.forEach(print.bind(null, path.concat(split(layer.route.path))))
  } else if (layer.name === 'router' && layer.handle.stack) {
    layer.handle.stack.forEach(print.bind(null, path.concat(split(layer.regexp))))
  } else if (layer.method) {
    console.log('%s /%s',
      layer.method.toUpperCase(),
      path.concat(split(layer.regexp)).filter(Boolean).join('/'))
  }
}

function split (thing) {
  if (typeof thing === 'string') {
    return thing.split('/')
  } else if (thing.fast_slash) {
    return ''
  } else {
    var match = thing.toString()
      .replace('\\/?', '')
      .replace('(?=\\/|$)', '$')
      .match(/^\/\^((?:\\[.*+?^${}()|[\]\\\/]|[^.*+?^${}()|[\]\\\/])*)\$\//)
    return match
      ? match[1].replace(/\\(.)/g, '$1').split('/')
      : '<complex:' + thing.toString() + '>'
  }
}

app._router.stack.forEach(print.bind(null, []))

That produces:

POST /api/v1/user
GET /api/v1/user
PUT /api/v1/user
GET /

dougwilson avatar May 12 '17 01:05 dougwilson

Hey, I'll take hacky over nothing any day . :)

I'll have to try this against our other apps and get back to you! Thanks for looking into this so quickly!!

bryanCoteChang avatar May 12 '17 01:05 bryanCoteChang

It's been working perfectly for our simpler projects, pretty confident it'll work for our more convoluted apps as well, but I will keep you posted. :)

I wanted to check in with you before I did it, but there's this stackoverflow post of others who have tried to get this working, would it be ok if I posted your hacky albeit effective solution?

bryanCoteChang avatar May 15 '17 18:05 bryanCoteChang

It's the only I found that realy works so far ... good job

urbinopescada avatar Jul 31 '17 10:07 urbinopescada

Thanks, @dougwilson! This is the only solution working. I hope the next version do not break this code :D

knnth avatar Nov 04 '17 12:11 knnth

Same topic discussed and addressed on StackOveflow.

daniele-orlando avatar Mar 07 '18 12:03 daniele-orlando

I've found this package, which seems to work properly: https://github.com/AlbertoFdzM/express-list-endpoints !

mathieutu avatar Oct 04 '18 10:10 mathieutu

I wrote a package to list middleware and routes mounted on an app quite a while ago: https://github.com/ErisDS/middleware-stack-printer

Maybe its useful for someone else.

ErisDS avatar Nov 20 '18 16:11 ErisDS

Why don't you try something like swagger using tsoa(it automatically generates a swagger.json file) which will have all the api liat including what parameters it takes. You can also hoat the same using swagge-ui-express locally. That will act as a better documents and will always auto update if you set up your run scripts correctly.

Just one note, tsoa requires Typescript.

surendra-y avatar Dec 16 '18 18:12 surendra-y

@surendra-y - the problem I've had with swagger & friends (and tsoa looks similar) is that they all depend on some sort of non-authoritative, duplicate source of route information: annotations, jsdoc, etc. Which only works long term if you're disciplined enough where you probably don't need it in the first place. The great thing about doing it via reflection is:

  • it's trivial to maintain
  • it's actually helped me find really dumb copy & paste bugs (like a route/resource was set up by copying another route wiring, and then changing the method... but not the route/resource), because it's generated from a truly authoritative source of truth. We'll not talk about who wrote the really dumb bug (me) ;)

That said, if whatever middleware does this also spits out the result in OpenAPI format - even better!

StuAtGit avatar Mar 15 '19 01:03 StuAtGit

Here's how I parsed out the express routes to a json array.

const endpoints = app._router.stack.filter(x=> x.route && x.route.path && Object.keys(x.route.methods) != 0).map(layer => ({ method :layer.route.stack[0].method.toUpperCase(), path: layer.route.path}));

Result [{ method: "GET", path: "/api/example }, {...}]

dondre avatar Oct 13 '19 19:10 dondre

@StuAtGit, maybe this will help you?

https://github.com/wesleytodd/express-openapi/

wesleytodd avatar Oct 15 '19 18:10 wesleytodd

@wesleytodd - 👍 That's a very clever solution. The kind of thing that seems obvious once you see, but as seen in this thread, not until then ;)

StuAtGit avatar Oct 15 '19 19:10 StuAtGit

I use this package, which gives both terminal and web view https://www.npmjs.com/package/express-routes-catalogue

NaveenDA avatar Oct 24 '19 07:10 NaveenDA

@dougwilson

Seems like that there are also some parasites on the internet, who steal your idea and sell them as their own...

https://medium.com/@stupid_arnob/get-all-api-path-with-method-in-a-single-api-request-f6116254ea1a

Uzlopak avatar Jan 17 '20 22:01 Uzlopak

Hi if someone is still searching, I rewrote the solution of @dougwilson in Typescript and made it output an array of strings of your routes so you can do with them what you wish.

function getRoutesOfLayer(path: string, layer: any): string[] {
    if (layer.method) {
        return [layer.method.toUpperCase() + ' ' + path];
    }
    else if (layer.route) {
        return getRoutesOfLayer(path + split(layer.route.path), layer.route.stack[0]);
    }
    else if (layer.name === 'router' && layer.handle.stack) {
        let routes: string[] = [];

        layer.handle.stack.forEach(function(stackItem: any) {
            routes = routes.concat(getRoutesOfLayer(path + split(layer.regexp), stackItem));
        });

        return routes;
    }

    return [];
}
function split (thing: any): string {
    if (typeof thing === 'string') {
        return thing;
    } else if (thing.fast_slash) {
        return '';
    } else {
        var match = thing.toString()
        .replace('\\/?', '')
        .replace('(?=\\/|$)', '$')
        .match(/^\/\^((?:\\[.*+?^${}()|[\]\\\/]|[^.*+?^${}()|[\]\\\/])*)\$\//)
        return match
        ? match[1].replace(/\\(.)/g, '$1')
        : '<complex:' + thing.toString() + '>';
    }
}

And then you can call it like this:

function getRoutes(app: Application): string[] {
    let routes: string[] = [];

    app._router.stack.forEach(function(layer: any) {
        routes = routes.concat(getRoutesOfLayer('', layer));
    });

    return routes;
}

TLouage avatar Apr 24 '20 13:04 TLouage

We are always listening 😀

ghinks avatar Apr 24 '20 13:04 ghinks

Actually I wrote it as an middleware and in a singleton pattern. This is useful if you dont add routes on runtime, every route has only one Routehandler. So you can save the routes into a Map one time and can have a fast lookup. I used it to match the routes to the RouteHandlers and now I can lookup what route a routehandler has.

Uzlopak avatar Apr 25 '20 10:04 Uzlopak

I wrote up a dependency free Typescript npm package that parses complex express apps and outputs a list of data per route and allows attaching meta-data to each route, if desired. https://www.npmjs.com/package/express-route-parser It also handles the cases that wesley todd's open api npm package runs into on some project configurations.

nklisch avatar Jul 29 '22 20:07 nklisch

@nklisch I appreciate your library very much. However, I am afraid it does not dig up to the middleware level. Take, for example, my app available here: I use three libraries, swagger-stats, express-status-monitor and swagger-jsdoc, which inject some route, respectively /swagger-stats/stats, /status and explicitly-defined /swagger route. However, it is not able to find them for a reason unknown.

Could you please take a look at this abnormal behaviour?

brunolnetto avatar Nov 14 '22 01:11 brunolnetto

@brunolnetto I'll take a look

nklisch avatar Nov 14 '22 04:11 nklisch

@brunolnetto For swagger ui try: router.use('/swagger"', swaggerUi.serve); router.get('/swagger", swaggerUi.setup(swaggerDocument)); instead of just one app.use() line.

But for swagger-stats, I looked into how they do their routing, and unfortunately they have a custom solution that doesn't use Expresses built-in route matching, seen here: https://github.com/slanatech/swagger-stats/blob/master/lib/swsInterface.js#L141-L149

This means it is impossible for someone to scan the Express stack to figure out these routes, as they are hidden inside custom logic, inside a middleware.

nklisch avatar Nov 14 '22 04:11 nklisch

Hey y'all – to my understanding NestJS uses express for routing – how can I grab the @Version decorator when getting routes? FWIW I'm using URI based versioning, so the routes look something like:

/foo/1.0/bar/baz however, when querering the router (using this is an example https://github.com/expressjs/express/issues/3308#issuecomment-300957572) – I see: /bar/baz (which is missing both the versioning prefix (foo) and the version number 1.0. Any help would be greatly appreciated...

m-graf avatar Mar 13 '23 19:03 m-graf

Is there anyone that renders it as an HTML file?

olawalejuwonm avatar Apr 13 '23 16:04 olawalejuwonm