What, exactly, is wrong with express?

An honest look into a frequently dismissed question

Posted 20 May 2024


Across the interwebs I’ve seen users ask whether the node web framework express should still be used. Invariably, a barrage of replies abound, passionately exclaiming “express is fine!”, “its battle tested”, “get over it”, “don’t just chase the new thing” etc.

However, none of these really provide a good explanation or answer at all, beyond essentially shaming our curious friend. There is a tendency for especially inexperienced developers to lack appreciation for stable and tested software, compared to flashy new stuff. But on the flip side, the jaded engineer who’s “been around for while” similarly never fails to suffer the exact opposite knee-jerk reaction: to reject the new.

It’s true that established practices have an inherent advantage that cannot be faked. Since it’s been around for a while, it’s been battle-tested and put through it’s paces - we can expect it to exist for a while longer. This so-called Lindy effect, is an inescapable truism of life, implicitly understood by people who possess any habitual tendencies whatsoever.

While green engineers are excited by novelty, and experienced engineers burned by the hotness of old, both disagree, they are both under the influence of biases independent to the actual subject in question.

An in this instance, it’s worth understanding whether express is truly worthwhile, or whether we should move on.


The Good

To briefly represent the pro-express position, it is all of the things established software is: battle-tested with proof-in-the-pudding. It’s age and poplarity also brings with it a comprehensive ecosystem. This is non-trivial. But it’s also true of any old tech that gained mainstream adoption.

Express has a lot to like, but I assume that by reading this you’re already familiar with most of it, so I’ll cut this part short.

The Bad

On the other hand: it’s effectively an abandoned project. It hasn’t received a release in many years and it’s next version has remained in beta for years too. This alone is not a problem, many will wager. “It’s already stable” they will say. This is true, but given it is presumably imperfect, it’s chances of improving are slim.

So while it generally lacks bugs - a very good thing - any antiquated approaches, error-prone APIs or otherwise dated or missing stuff will likely go unresolved, at least for a while. Again, this alone isn’t bad. It’s bad when we consider the ugly parts of express.

The Ugly

Express was created before Typescript existed at all. It was created during a time when relentlessly modifying .prototype was considered reasonable practice. It was created before Promise, let alone async/await. When express was created, callbacks were the bread and butter of JS, and to this day the memory of “callback hell” lives on when writing express.

None of this makes express unusable, de-facto dead or even irrelevant. Clearly people can and do use express to this day, including myself.

But to say this is all inconsequential is entirely disingenuous. The world express was birthed into is the origin of it’s problems today. Again, these problems don’t make it untenable or broken. But they do make things unnecessarily complex or unruly. Consequently, much of the ecosystem suffers the same fate too.

The Problems With Express

The below problems are not unique to express, but are nonetheless present. In fact, many of these problems have been replicated since express, probably imitating it, consciously or otherwise.

The Request and Response Object

Try to instantiate an express request object. You can’t. Or at least not easily. Request is a bespoke class created by express. In fact it’s not even a class, because they didn’t exist in JS till later on. It is merely an object extending the node IncomingMessage prototype, which then mutably adds the various methods and properties you’re used to.

So not only can we not easily instantiate the very input used all over our app, without resorting to third party libraries, but we’ve also deviated from the standard node IncomingRequest shape. That laid the groundwork for a requiring framework-specific ecosystem, as opposed to merely a runtime-specific one. These problems are mirrored in the Response object too.

Mutablity

The ability to mutate req and res can become a problem. In fact, it’s not just possible, it’s idiomatic express code to mutate these. Mutability is obviously not categorically bad, but especially with third party code, understanding what’s going on becomes borderline impossible when flippantly mutating state along a complicated pipeline of middleware and handlers.

Imperativeness

Like mutability, express also employs heavy use of imperative code, as opposed to declarative code. Arguably, express routing is declarative (and consequently the best part of express imo), but the actual business logic is resoundingly imperative. Callbacks are one component of this.

For example we don’t return a response, we call res.send. And if we don’t do this properly we run into…

Response Fragility

In lack of a better term, express deals with returning responses in a strange manner. Not infrequently, express will complain that you’re trying to set headers after the response has already resolved or returned. This is fair enough, conceptually: you can’t update a response if it’s already gone out the door. But the way express allows us to modify already terminated responses is odd in today’s world, not to mention error-prone.

Any express handler or middleware can call res.send at any point. Downstream handlers may then continue to run, if return and next() are not properly used, often causing erroneous behaviour like modifying an expired response. This is not a “bug”, but is certainly far from ideal and causes non-trivial issues.

Callbacks

Aside from general imperativeness, express uses callbacks for core functionality, namely next(). While, again, this was perfectly adequate in it’s day, it certainly makes some things more complex than necessary today. For starters, it complicates the execution pipeline, by being able to run the next function arbitrarily. Secondly, this replaces JS-native functionality like just throwing an error.

Fundamentally, this approach was put forward as a clever way of enabling async before promises existed. This leads us to…

Lack of native Async/Await

In the year of our lord, 2024, the stable version of express (v4) still does not natively support async functions. It’s possible to use promises, async and await - true. But any unhandled promises will crash your app. And even if you manage to diligently avoid this, the underlying “callback” style is unavoidably embedded into the very fibre of express and it’s ecosystem.

As such, natively supporting async/await is essentially impossible, as the much-lauded ecosystem depends so much on the anachronisms of older express.

Poor Typescript Support

In a similar vein, Typescript has questionable support in the world of express. While not everyone cares about this, the first-class express types themselves are not particularly good. They are tacked-on as a .d.ts instead of express being created in Typescript. Moreover, the type definitions are often a bit awkward or not exported at all.

Like async/await this issue is compounded in the context of the ecosystem, where it’s very much a gamble whether an express plugin has type definitions or not.


A Word on Ecosystem

Ecosystem is a valid, but often overused, defence of incumbent technology. Of course existing technology will have a greater ecosystem, but it’s not always the case that this is meaningful. For example, React has a much larger ecosystem than Svelte, but Svelte itself encompasses much of what would otherwise be a library in React (whether it’s sufficient depends on your use case).

In the case of express, much of the essential ecosystem could be reimplemented relatively easily, while the remainder would be redundant anyway. In the case of both Svelte and express, ecosystem (or lack thereof) is not necessarily a negative. Sometimes the best ecosystem is no ecosystem.


Conclusion

Without devolving into more opinionated critiques, I think this list adequately articulates the perhaps implicit feelings of dissatisfaction people feel when using express. The above list is, in my opinion, a fairly objective one.

As we’ve established, express certainly has some pitfalls. Under honest scrutiny, it isn’t perfect. Whether these imperfections are meaningful to you, however, is much more subjective. And whether there are better alternatives you should use instead, is further dependent on the rest of the ecosystem, on top of your preferences.


A note on webroute

This post is one of several, spurring the creation of an open source project called webroute. It is a toolkit built on web-standards for building server side apps with Type/JavaScript. Webroute employs many of the ideas and philosophies presented in essays like this one. Please check it out, raise an issue or read the docs, if this sounds interesting.