Ep.06: Express - App versions (Node Backend Framework Comparison)
Teolin Codreanu
Software Developer | Microservices / APIs in Node.js, JavaScript, TypeScript, Nest.js / Koa / Express etc
This article is part of an in-depth comparison series of the top Node frameworks. We'll cover all the important aspects: market share, learning curve
Table of contents
While building the Koa version of the app, I realised that it's going to be quite difficult to observe the differences since the app is split across so many files. So, I rebuild the app as a single-file version of the app. And it can be even shorter if we give up the separation of concerns
How can we de-modularize the app? Well, the first issue we're going to have is function collision names, since for each route there will be a controller and service function with the exact same name. For example, we have the function registerUser() in users.controller.js which calls function registerUser() from users.service.js. Put those in the same file and watch it burn! so, I prefixed all the service functions with 'NameService_', (Users, asks, Lists). This is a really awkward solution, don't do that! It's considered a code smell, it tells you that those functions belong in a class or module somewhere. So then, I thought, why not also build an OOP (Object Oriented Programming) version of the app?
Fast-forward 15 minutes and we now have 3 versions of the Express Todo app. To avoid confusion, I realised we need a way to keep these forks separate, in their own folders. In fact, to be able to easily compare the Express App with the Nest.js App we could also use a TypeScript version of it, as Nest.js (and Next.js) are both TypeScript-based. So, we would have either modular or single-file versions in JavaScript with Functional Programming
This will leave us with a set of up to 10 versions, we'll call them the base versions from Stage Zero.
In stage one we want to try on-disk storage, in stage two - a real database, then we want a high compute set, next we want to add features like authentication, input validation or logging. It's going to be a mess if we don't organise them. I won't replicate all 10 base versions for the later stages, don't worry, I will use the most comparable pair (the TypeScript OOP versions). Nor will I replicate the base versions across all frameworks, I'll only create them for Express.
So, let's number them. Stage zero will stay in folders src-000 to src-009, stage one in folders src-010 to src-019 etc. For each stage we're going to have 5 possible pairs of forks (10 in total), where 'xx' is the placeholder for the stage number:
I repeat, this doesn't mean I'll build 10 versions for each stage for each framework, I'm not that crazy (well, neighbours disagree, but...). I'll build the reference ones in Express, and then only the ones needed for benchmarking and comparison in the other frameworks.
In Stages 00x-04x, I'll benchmark the impact of using various types of hardware resources on performance and speed:
From here on, we'll revert to in-memory storage, to take network, I/O and compute out of the equation, and I'll incrementally add typical functionalities:
So, as an example, version 072 will be a modular OOP .ts version, with all the functionalities up to 070 security - authentication added on top.
领英推荐
Flat OOP version (src-004, src-005)
I know that sometimes you see a pointless tug-of-war between OOP (Object Oriented Programming) and FP (Functional Programming). Just keep in mind that OOP and FP mean a lot more than just organizing your code in one way or another, and you don't need to choose a paradigm based on this. In fact, you should not choose a paradigm! They each serve their own purpose, and they can complement each other well, just like AOP (Aspect Oriented Programming) does with both of them (the principle of separation of concerns that guided our initial modularisation of the code hails from AOP).
The imports and setup are the same for both (although you could create a class wrapper over the app, but I see little point in that:
The biggest difference is in the controllers and services. We simply group up all the functions of the former users.controller.js into a class UserController, and convert the functions into static methods. The alternative would be to convert them into public methods, but then you'd need to instantiate the class for every request. If you have 10k concurrent requests, you'll clog the memory with 10k copies of a new UserController, which is not pretty, your memory will run into the bushes and take a leak. Nope. So, with all of them collapsed, it looks like this:
While we can still distinguish between the routing functions on the left, it's so much more readable when they belong to a class (right). This will also make it more clear when we call the controller functions/methods later on:
As you can see, the only difference between the two is how we call the controllers:
We apply the same treatment to classes, thus removing the aforementioned code smell:
Without classes (left), we resort to breaking clean code principles around naming, and it's a total mess, and difficult to read. With classes, all is clear.
I won't expand the code, as there's no change there, except for how we call the service functions (i.e. usersService_createUser() vs UserService.createUser()).
So, there you have it, a functional vs object-oriented comparison of the same Express app. This will come in handy later on when we compare it with other frameworks.
You don't have to take my word for it, do test it yourself. Clone the repo and give it a try.
Updates
NOTE: in the meantime, I decided to merge the routers, as it really does not make sense to have 3 of them in a single file version of the app. It will make it less comparable with the main app implementation, but it's easier to read overall. The screenshots however will preserve the version with 3 routers.