First things first: I think the way you implemented it is really simple and easy to understand at a glance. There’s basically nothing significant to take away from it. It’s perfectly good code as it stands, before the “list all routes” requirement came up.

Before you knew you’d want to list routes, any abstraction would have been premature, and unlikely to be helpful because it would have possibly been the wrong abstraction. Chances are you’d have had to redo it anyway the moment your “list all routes” requirement arrived.

> The simplest way to do what I want here would be to query the code AST for all conditionals in this function, and print them out.

Yes and no.

Simplicity is not a one-dimensional metric. You’d keep the simplicity of your switch statement, but introduce the complexity that is AST parsing. You’d also introduce action-at-a-distance. What if another developer decides to change `(url.pathname === '/bar')` into `(url.pathname ~== /bar\/(?<subpath>baz|qux))`, unaware they’re going to break the assumptions of your AST-based listing feature?

There are several (equally simple, or even simpler) methods to implement your requirement other than resorting to AST parsing.

But what actually makes a good solution is likely to hinge on particular details, context, and requirements of your project. For example, I used to have a similar requirement in a Java project a few years ago. I wanted us to be able to enumerate our API endpoints at development time, and generate a JSON schema of a specific endpoint for reference. What I ended up doing is introduce a custom annotation, and write a plug-in for our build system that added a `gradle api:listEndpoints` task and a set of `gradle api:generateSchemaForFoo` tasks that integrated well with our IDE and could be used from the command line, too. In retrospect, I think my approach was perfectly fine for the specific situation we tried to tackle. However, that approach would likely have been a poor, hopelessly overengineered solution for other contexts.

With all that out of the way:

> That is, my routes now become objects. But if I have a more complicated conditional to route match instead of just `url.pathname`, then things start becoming way more complicated.

> I find this situation often when coding, and I find it creates so much unnecessary complexity.

Maintaining a list of route objects doesn’t have to be complex.

I know you didn’t ask for implementation advice, but here’s one anyway just to illustrate my point (untested):

const routes = {
"/foo": handler.doFoo,
"/bar": handler.doBar,
"myComplexCondition": {
description: "URL path that includes the magic word",
matches: (url) => url.pathname.includes("xyzzy"),
handle: handler.doXyzzy,
},
"/routes": handler.printAllRoutes,
};

function handleRequest(req) {
for (const [id, routeExpression] of Object.entries(routes)) {
const routeConfig = {
id: id,
description: id,
matches: (url) => url.pathname === id,
handle: () => { throw new Error("Missing `handle`"); },
...(
routeExpression instanceof "string"
? { handle: routeExpression }
: routeExpression
),
};
const url = new URL(req.url);
if (routeConfig.matches(url)) {
return routeConfig.handle(url);
}
}
throw new Error("No route matches");
}