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.
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 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.
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 [email protected] | ✅ | ❌ |
Async readers | âś… | âś… |
Request body parsing | âś… | âś… |
Logging – pluggable, out-of-the-box-solution for server/service logging. | ✅ | ✅ |
RxJS support | âś… | âś… |
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
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
.
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!”
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.
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.
Hardware:
Conditions:
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
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 5000000 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.
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.
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 and mobile apps, recording literally everything that happens while a user interacts with your app. 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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
3 Replies to "Marble.js vs. Express.js: Comparing Node.js web frameworks"
Really interesting. Thanks David
Really like this article! Never knew about Marble.js
Although Express itself does not have Typescript definitions, the @types/express module does a very nice job of providing them, and integrates Express very nicely into my Typescript-based servers.