Written by David Ekanem ✏️
In this article, we’ll compare Express.js — one of the most commonly used server-side frameworks for building web applications with Node.js — against Marble.js, another, newer, reactive server-side Node.js framework.
Prerequisites
If you’re not familiar with functional reactive programming (FRP), you may want to pause before reading this. I’d recommend Conal Elliott’s work, which contains a lot more precise information.
The general gist of FRP is that it’s a general paradigm for describing dynamic (a.k.a., time-varying) information and the successive side effects of sequential execution. Conal Elliott captures the meaning of FRP with this statement:
FRP is concurrent without running afoul of the theoretical and pragmatic rats’ nest that plagues imperative concurrency.
Andre Staltz, the author of the Cycle.js framework and a core contributor to RxJS, gives this wonderful explanation as well:
Reactive programming raises the level of abstraction of code, so one can focus on the interdependence of events that define the business logic, rather than having to constantly fiddle with a large amount of implementation details.
This allows for explicit execution of our programs. Once execution is started on a process on a new thread, it should be capable of being controlled.
Now, let’s get on with our comparison.
Marble.js vs. Express.js: A basic comparison
What is Marble.js?
Marble.js is a functional reactive Node.js framework for building server-side applications. It’s based on TypeScript and RxJS and relies on the reactive paradigm, with functional sugar on top of the Node platform.
The main building block of Marble.js is an Effect, or a function that returns a stream of events.
Due to the event-based nature of Marble.js, when a service performs an action that other services might be interested in, that service produces an event, which is a record of the performed action. Other services consume those events to perform the actions needed as a result of the event.
An Effect is suitable for implementing distributed event-handling systems, and as such, Marble.js can be used for general message-driven architecture (MDA) solutions, like WebSockets or microservices, in addition to operating over basic HTTP protocols.
What is Express.js?
Express.js is a fast, flexible, and minimalistic Node.js web development framework that can design single-page, multi-page, and hybrid applications with a set of robust features.
Express offers lots of middleware and other inbuilt support to make API creation easy, as well as support for third-party modules for interfacing with databases. These advantages allow you to use nearly any form of database with Express.
For a high-level comparison of Marble v3 and Express v4, let’s look at the table below.
Features and Criteria | Marble.js v3 | Express v4 |
Minimum supported Node.js version | ≥v8.0 | ≥v0.10.0 |
TypeScript support | ✅ | ❌ |
Inbuilt middleware | ✅ | ✅ |
Core package size | 102 kB | 208 kB |
HTTP helpers | ✅ | ✅ |
Modular design | ✅ | ✅ |
I/O handling | ✅ | ✅ |
Support for fp-ts@2.0 | ✅ | ❌ |
Async readers | ✅ | ✅ |
Request body parsing | ✅ | ✅ |
Logging - pluggable, out-of-the-box-solution for server/service logging. | ✅ | ✅ |
RxJS support | ✅ | ✅ |
Creating our Marble.js and Express.js directories
We are going to create a “Hello, World!” program using Marble.js and Express.js to outline how routing is handled in Marble.js and Express.js and run a benchmarking tool on the created web applications to see which of the two frameworks is faster.
As we noted above, Express.js requires Node ≥v0.10.0, while Marble.js requires Node ≥v8.0. I will be using Yarn as my package manager and the Windows operating system.
In your desired project directory. create the folder that will hold your Marble application.
$ mkdir MarbleJsApp
Open up your MarbleJsApp
folder in your favorite code editor; I will be using VS Code.
$ code
Once it’s open, open the terminal window and run the command:
$ yarn init -y
Install the @marblejs/http
module:
$ yarn add @marblejs/core @marblejs/http fp-ts rxjs
Every @marblejs/* package requires @marblejs/core + fp-ts + rxjs to be installed first.
In another project directory, create the folder that will hold your Express application.
$ mkdir ExpressJsApp
Open up the ExpressJsApp
folder.
$ code
In the terminal window, run the command to create the package.json
file.
$ yarn init -y
Now, open your ExpressJsApp
folder and the terminal window and install Express.
$ yarn add express
Creating a Marble.js server
Since version 4.0, the @marblejs/http
package contains all HTTP-related APIs, and we need to install this package in order for Marble to carry out the two basic configurations we need for creating our server-side application. The two basic configurations are the HTTP listener definition and the HTTP server configuration.
We are going to create three base files in our project’s root directory:
-
index.ts
-
http.listener.ts
-
api.effects.ts
Open up the terminal window and input these commands to create the files:
$ touch index.ts http.listener.ts api.effects.ts
In our index.ts
file, we first create a Marble app instance with the createServer
method from the @marblejs/http module
; the createServer
method is a wrapper around the Node.js server creator.
import { createServer } from '@marblejs/http';
import { IO } from 'fp-ts/lib/IO';
import { listener } from './http.listener';
const server = createServer({
port: 1337,
hostname: '127.0.0.1',
listener,
});
const main: IO<void> = async () =>
await (await server)();
main();
To test-run the newly created server, we install the TypeScript compiler and ts-node
:
$ yarn add typescript ts-node
Add the following script to our package.json
file:
"scripts": {
"start": "ts-node index.ts"
},
Start the server:
$ yarn start
The server should be running. You can check using your browser by visiting http://localhost:1337
.
Creating an Express.js server
We create an index.js
file in our ExpressJSApp
folder and create a server that listens on port 1338
for connections.
const express = require("express");
const app = express();
const port = 1338;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
Run the Express application with the following command:
$ node app.js
We have been able to create our server with only a few lines of code — a sneak peek into the power and simplicity of Express.
We can open up our browser tab and paste in the URL http://localhost:1338
. The app should respond with “Hello, world!”
Marble.js vs. Express.js: Routing
Let’s walk through how to handle routing in both Express.js and Marble.js.
Express.js contains a lot of helper functions in their req
and res
objects, which comes with methods such as res.send
, res.download
, res.redirect
, and so on.
const express = require("express");
const app = express();
const port = 1338;
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
By comparison, defining routes in Marble is done through defining an API effect. As a reminder, an effect is simply a stream of events that responds to incoming requests. In our case, it would return our “Hello, world!” message.
To build out our route in Marble, we open the http.listener.ts
file to define our httpListener
program. This is the basic starting point of every Marble application, as it includes the definition of all global middleware and API effects.
We are then going to install two more important modules in our application: one for logging and one for parsing.
Install middleware packages with the following command:
$ yarn add @marblejs/middleware-logger @marblejs/middleware-body
And define the httpListener
like so:
import { httpListener } from '@marblejs/http';
import { logger$ } from '@marblejs/middleware-logger';
import { bodyParser$ } from '@marblejs/middleware-body';
import { api$ } from './api.effects';
const middlewares = [
logger$(),
bodyParser$(),
];
const effects = [
api$,
];
export const listener = httpListener({
middlewares,
effects,
});
Using the Effects interface, we can define API endpoints or event handlers. We are going to create an API effect that returns “Hello, world!” to the user.
Open up the api.effects.ts
file. Here, we’ll create our first endpoint. In order to route our first httpEffect
, we have to define the path and method with which the incoming requests should be matched.
Here, we are specifying the matchPath
property as ("/")
. Our method with the matchType
property is a GET
method.
Defining our first endpoint in api.effects.ts
import { r } from '@marblejs/http';
import { mapTo } from 'rxjs/operators';
export const api$ = r.pipe(
r.matchPath('/'),
r.matchType('GET'),
r.useEffect(req$ => req$.pipe(
mapTo({ body: 'Hello, world!' }),
)));
If we go back to http://localhost:1337
, we should see our “Hello, world!” message being returned.
Comparing performance benchmarks
Hardware:
- Laptop: Lenovo Thinkpad W540
- CPU: Intel ® Core(™) i7-4800MQ CPU @ 2.70GHz
Conditions:
- 125 connections
- 5000000 requests
Benchmarking Marble.js HTTP performance
For testing, I’ll use the Go Bombardier package, which runs 5000000 requests with 125 concurrent connections with the following command:
Run the following command, specifying the URL of the Marble.js project.
$ bombardier -c 125 -n 5000000 http://127.0.0.1:1337/
Below is our result from running the test on our Marble.js application.
Bombarding http://127.0.0.1:1337/ with 5000000 request(s) using 125 connection(s)
5000000 / 5000000 [=====================================================================================] 100.00% 2355/s 35m22s
Done!
Statistics Avg Stdev Max
Reqs/sec 2368.27 790.92 6658.29
Latency 53.07ms 12.42ms 601.59ms
HTTP codes:
1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 604.92KB/s
Benchmarking Express.js HTTP performance
Run the following command, specifying the URL of the Express.js project.
$ bombardier -c 125 -n 5000000 http://127.0.0.1:1338/
Below is our result from running the test on our Express.js application.
Bombarding http://127.0.0.1:1338/ with 5000000 request(s) using 125 connection(s)
5000000 / 5000000 [================] 100.00% 5738/s 14m31s
Done!
Statistics Avg Stdev Max
Reqs/sec 5745.13 1164.50 34982.51
Latency 21.78ms 4.02ms 411.92ms
HTTP codes:
1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 1.65MB/s
The purpose of the benchmarking test is all about performance and comparing the performance of the two frameworks. With this in mind, the most important metric is the request count per second.
When considering this measurement, we want the highest possible value because the more requests per second the application can handle, the faster the application.
Here are the results of our benchmark tests:
Framework | Marble.js | Express.js |
---|---|---|
Requests per second | 2368.27 | 5745.13 |
Latency (ms) | 53.07 | 21.78 |
Total time to handle 5,000,000 requests | 35 mins | 14 mins |
By this metric, Express.js has a higher latency. Looking at our benchmark test results, the Express.js application performed much better than the Marble.js application.
Conclusion
Express.js is great for creating simple applications, as we have seen from creating our “Hello, world!” program, building out simple routing, and ease of learning.
In our benchmarking test, Express turned out to be much more capable of handling a higher number of requests than Marble.js.
Marble.js provides support for TypeScript with great support for the higher-level functions provided by RxJS, and with its dedicated @marblejs/messaging
module, Marble.js offers a robust and unified architecture for building event-based microservices.
Express.js is great for building RESTful APIs, but when it comes to building applications with an event-driven architecture, Marble.js excels.
Express.js is still popular and will continue to be used in major projects and industries, but Marble.js is quite popular within the proponents of functional reactive programming. Hopefully, we will continue to see an increase in Marble’s usage among developers who are looking to incorporate its asynchronous nature into applications that rely on servers to handle heavy work and complex calculations without freezing the main thread.
200’s only ✔️ Monitor failed and slow network requests in production
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Top comments (0)