Ep.06: Express - App versions (Node Backend Framework Comparison)

Ep.06: Express - App versions (Node Backend Framework Comparison)

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, ecosystem, security and more. To compare them properly, we will build the exact same app in all these frameworks (plus Vanilla Node), observe all the steps along the way and then benchmark them as we progressively add more functionalities.

Table of contents

  1. Market Share Distribution Analysis: Picking the most used frameworks
  2. Planning Phase: Use cases, Minimum Viable Product requirements, Architecture
  3. Tools & Setup: Getting started and setting up the tools and environment
  4. Scaffolding: Project Settings and Configuration, Common template repo
  5. Express: basic app (MVP)
  6. Koa: basic app (MVP)
  7. Express Flavors: basic app (MVP) in multiple variants: OOP, FP, TypeScript, single-file etc
  8. Nest.js: basic app (MVP)
  9. Fastify: basic app (MVP)
  10. Next.js: basic app (MVP)
  11. Vanilla Node: basic app (MVP)
  12. Functional testing with Postman + Newman


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 and merge the service functions into their corresponding controller functions. Let's not go that far tho...

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, JavaScript with Object-Oriented Programming, TypeScript with OOP, and we would sometimes have a frameworks-specific version (modular only), possibly more.

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:

  • src-xx0: modular OOP .ts - most comparable - our baseline.
  • src-xx1: single-file OOP .ts
  • src-xx2: modular OOP .js - won't do
  • src-xx3: single-file OOP .js
  • src-xx4: modular OOP flat .js
  • src-xx5: single-file OOP flat .js
  • src-xx6: modular FP .js
  • src-xx7: single-file FP .js
  • src-xx8:
  • src-xx9: modular framework specific

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:

  • 00x: in memory
  • 01x: on disk
  • 02x: high compute
  • 03x: database
  • 04x: all resources

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:

  • 05x: error handling
  • 06x: input validation/sanitisation
  • 07x: security: authentication
  • 08x: security: authorisation
  • 09x: security: crss/cqrs/others...
  • 10x: middleware: logging
  • 11x: middleware: undetermined yet
  • 12x: documentation: openApi / swagger
  • 13x: others, I was thinking about testing, but that is pre-production. We'll see.

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:

  • directly, as a function name: for example 'createUser' (left)
  • via class, as a method name: same example, 'UserController.createUser' (right)

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.

要查看或添加评论,请登录

Teolin Codreanu的更多文章

社区洞察

其他会员也浏览了