javascript

Diving Into Reactive Programming in Node.js

Antonello Zanini

Antonello Zanini on

Diving Into Reactive Programming in Node.js

Boosting the scalability of your backend applications often means rethinking how you manage asynchronous data. That’s where reactive programming comes into play: a paradigm that treats data streams as first-class citizens, allowing your code to respond to data changes as they occur.

While Node.js wasn’t built with reactive programming in mind, libraries like RxJS and Bacon.js support that approach. When used right, they can improve your event-driven architecture and power more responsive microservices.

In this guide, you'll learn what reactive programming is all about, explore how it fits into the Node.js ecosystem, and we'll walk through practical examples to see it in action.

Time to go reactive with Node.js!

What Is Reactive Programming?

Reactive programming is a declarative paradigm centered around asynchronous data streams and the propagation of change. The idea is that reactive systems automatically respond as data changes.

Node.js doesn't natively support reactive programming, but it can be implemented using third-party libraries. In recent years, this paradigm has gained popularity in frontend development for implementing an optimistic UI and efficiently managing component state. However, the approach is also well-suited for backend systems that deal with real-time data, data streams, and event-driven architectures.

As a result, reactive programming is a compelling approach for building highly scalable and responsive web applications that require non-blocking, event-driven behavior.

Understanding Reactive Programming in a Node Backend

In frontend development, understanding reactive programming is fairly simple. That’s because many JavaScript frameworks are either built around it or offer some level of support.

For example, imagine you're building a real-time stock price dashboard. In an imperative style, you’d write code that polls the server for updates at regular intervals. Instead, in reactive programming, you subscribe to a data stream. Whenever a new price comes in, the UI will update automatically.

But what about the backend? Things can be a bit trickier here...

Suppose you're building a Node.js backend for an e-commerce platform that handles real-time order processing. Instead of relying on a central orchestrator, you could apply reactive programming principles using a message queue like Kafka.

Here's how your reactive Node backend would work:

  1. A customer places an order.
  2. That event is pushed to a message stream.
  3. Multiple microservices (e.g., payment, inventory, shipping) subscribe to that stream and react independently:
    • The payment service processes the transaction.
    • The inventory service updates the stock count.
    • The shipping service prepares the dispatch.

All of this happens asynchronously and independently, with no central controller managing the flow. A decoupled, event-driven architecture lies at the heart of reactive programming on the backend.

Pros and Cons of Reactive Programming in Node

Now that you understand what reactive programming is, we'll dive into its benefits and drawbacks when applied to a Node.js backend.

Pros

The pros of Node reactive programming include:

  • Enhanced scalability: Reactive backends can scale easily under varying loads by responding to data as it flows. This event-driven model is ideal for handling multiple asynchronous tasks in parallel, especially in I/O-heavy Node.js applications.
  • Reduced tight coupling: Reactive programming decentralizes control flow, removing the need for tightly coupled orchestrators. This promotes loosely coupled microservices that interact asynchronously and more flexibly.
  • Improved fault tolerance: Reactive libraries typically offer rich built-in features for handling errors in asynchronous operations. Errors can be isolated to specific asynchronous operations, preventing a single failure from crashing the system and helping you build fault-tolerant Node applications that recover gracefully.

Cons

While embracing reactive programming has several benefits, it also introduces trade-offs, such as:

  • Increased complexity: Building and maintaining reactive systems requires a thorough understanding of asynchronous programming, streams, and event loops. That can steepen the learning curve for many backend developers.
  • Code duplication: Decentralizing the control flow might lead to redundant logic being repeated across multiple microservices.
  • Debugging challenges: Tracing bugs in reactive systems can be difficult due to the non-linear, event-driven flow of data.

Core Concepts of Reactive Programming in Node.js

The JavaScript community has been discussing the inclusion of reactive programming features for years. While there are ongoing ECMA TC39 proposals, nothing has been officially implemented in Node.js yet.

Thus, if you want to embrace reactive programming in Node.js, you’ll need to rely on external libraries. These packages implement the reactive programming paradigm, which is based on five core concepts:

  • Observable
  • Observer
  • Subscription
  • Operators
  • Subjects

Time to explore them all!

Observable

An Observable represents a stream of asynchronous data that emits values over time:

JavaScript
const stream = new Observable((subscriber) => { // emit a value "Hello, World!" to the stream subscriber.next("Hello, World!"); // emit other values... // mark the stream as complete subscriber.complete(); });

In Node, an Observable can handle a variety of data sources, from HTTP requests to file system events. As you're about to see, you can subscribe to an Observable to react whenever it emits values.

Observer

An Observer is an object that defines how to handle emitted values from an Observable. It provides callbacks for responding to data events, such as:

  • next: Called whenever the Observable emits a new value.
  • error: Called if an error occurs in the stream.
  • complete: Called when the Observable has finished emitting values and no more data will arrive.

These methods allow the Observer to handle different stream states, as in the example below:

JavaScript
const observer = { // to handle the emitted values next: (value) => console.log("Next value:", value), // to handle any error that occurs in the stream error: (err) => console.error("Error:", err), // to handle when the Observable completes its data emission complete: () => console.log("Stream complete!"), };

Subscription

A Subscription represents the execution of an Observable, with the emitted data managed by the Observer. It allows you to control the flow of the data stream and how it's consumed:

JavaScript
// subscribe to the Observable using the Observer stream.subscribe(observer);

This starts the execution of the Observable, and the Observer will begin receiving and managing the emitted data as defined in its callbacks.

You can also unsubscribe from the Observable to stop listening to the data, as shown below:

JavaScript
// unsubscribe from the Observable after 10 seconds setTimeout(() => subscription.unsubscribe(), 10000);

Operators

Operators are functions to transform, filter, or combine data streams. They enable you to process and manipulate the data emitted by an Observable.

Some common reactive operators include:

  • map: Transforms the data emitted by the Observable.
  • filter: Filters the emitted data based on a specified condition.
  • merge: Combines multiple Observables into a single stream.
  • concat: Concatenates multiple Observables, emitting values in sequence.
  • reduce: Aggregates emitted values into a single result.
  • take: Limits the number of emissions from the Observable.

For example, see how to apply map() to a data stream:

JavaScript
of(1, 2, 3) .pipe(map((x) => x * 2)) .subscribe({ next: (x) => console.log(x) }); // output: 2, 4, 6

The above snippet creates an Observable that emits the values 1, 2, and 3. The pipe() function applies the map operator, which multiplies each emitted value by 2. As a result, 1 becomes 2, 2 becomes 4, and 3 becomes 6. Finally, the subscribe() method applies the Observer to log the transformed values to the console.

Subjects

A Subject acts both as an Observable and an Observer. It allows multicasting values to multiple subscribers, meaning all subscribers will receive the same value when it is emitted:

JavaScript
const subject = new Subject(); // Create a new instance of Subject // subscribe the first observer subject.subscribe({ next: (value) => console.log("Message:", value), // log the message }); // subscribe another observer subject.subscribe({ next: (value) => console.log("Message length:", value.length), // log the length of the message }); // emit a new value to all subscribers subject.next("Hello, World!"); // the two observers will receive and log the message and its length

This mechanism is useful when you want to broadcast data to different parts of your Node backend.

Best Libraries for Reactive Programming with Node.js

There are a few libraries that support reactive programming in Node.js, but many of them are either deprecated, no longer maintained, or have very limited adoption. Realistically, the reactive programming landscape in Node.js boils down to just two major libraries: RxJS and Bacon.js.

If you’re curious about how these two libraries stack up against each other, take a look at the summary table below:

AspectRxJSBacon.js
SupportBrowser, Node.jsBrowser, Node.js
Development LanguageTypeScriptTypeScript
Observable ModelSingle Observable typeDistinct EventStream and Property types
Observables SupportBoth cold and hotOnly hot
Error HandlingErrors typically terminate streamsErrors do not terminate streams
PerformanceHighAverage
Use Case SuitabilityComplex pipelines, state management, real-time appsSimpler applications and UI event handling
GitHub Stars31.5k6.5k
NPM Weekly Downloads~68 million~12k
Bundle Size4.5 MB (unpacked), 69.6 kB (minified)722 kB (unpacked), 41.2 kB (minified)

Let’s compare them head-to-head!

RxJS

RxJS is a powerful library for composing asynchronous and event-driven scenarios using observable sequences. It exposes a core Observable type and related types such as Observer, Scheduler, and Subject, along with a large set of operators inspired by array methods like map, filter, and reduce. It enables you to handle asynchronous events as streams.

  • Key aspects:
    • High performance, optimized from the ground up
    • Uses a single Observable type
    • Supports both cold observables (creating a new producer for each subscriber) and hot observables (sharing a producer across all subscribers)
    • Errors typically terminate streams (but can be caught and handled)
    • Framework-agnostic (integrates with Angular, React, and more)
    • Rich ecosystem and extensive set of operators
    • Ideal for complex data pipelines, state management, and real-time systems
  • Support: Browser, Node.js
  • Development language: TypeScript/JavaScript
  • GitHub stars: 31.5k
  • NPM weekly downloads: ~68 million
  • Bundle size: 4.5 MB (unpacked size), 69.6 kB (minified)

Bacon.js

Bacon.js is a functional reactive programming library for JavaScript and TypeScript. Its goal is to transform messy event-driven code into clean and declarative data flows. It shifts the focus from handling individual events to working with continuous event streams.

  • Key aspects:
    • Observables are heavier and less performant than RxJS
    • Distinguishes between EventStream (discrete events) and Property (continuous values)
    • All streams are hot: shared among all subscribers
    • Errors do not terminate streams
    • Follows a syntax based on jQuery and Zepto.js
    • Best suited for simpler apps and UI event handling
  • Support: Browser, Node.js
  • Development language: TypeScript
  • GitHub stars: 6.5k
  • NPM weekly downloads: ~12k
  • Bundle size: 722 kB (unpacked size), 41.2 kB (minified)

Let's finally take a quick look at the difference between imperative and reactive programming.

Reactive Programming vs Imperative Programming

Imperative programming is about describing how to do things. You write explicit instructions to manipulate data and manage control flow. In contrast, reactive programming is declarative. It focuses on what should happen in response to data changes or events, opening the door to a more flexible and event-driven approach to development.

For example, here’s a Node.js imperative snippet to handle two asynchronous operations based on HTTP requests:

JavaScript
const axios = require("axios"); async function fetchData(userId) { try { // fetch user data from API const userResponse = await axios.get( `https://api.example.com/users/${userId}` ); // if the user can post, fetch the posts if (userResponse.data.canPost) { const postsResponse = await axios.get( `https://api.example.com/posts?userId=${userResponse.data.id}` ); // example of an operation on the retrieved posts console.log(postsResponse.data); } } catch (error) { console.error("Error:", error); } } // call the function with userId=5 fetchData(5);

This snippet first fetches user data. Once that completes, it checks if the user can post and then fetches the posts based on the user ID. The results are logged after both requests are complete.

Now, let's achieve the same result using RxJS with reactive programming:

JavaScript
const axios = require("axios"); const { from } = require("rxjs"); const { switchMap } = require("rxjs/operators"); // fetch user data as a stream from(axios.get("https://api.example.com/users/5")) .pipe( // map produced values to a new observable switchMap((userResponse) => { // only fetch posts if the user can post if (userResponse.data.canPost) { return axios.get( `https://api.example.com/posts?userId=${userResponse.data.id}` ); } else { return []; // return empty array if user cannot post } }) ) .subscribe({ next: (postsResponse) => { // example of an operation on the retrieved posts console.log(postsResponse.data); }, error: (err) => { console.error("Error:", err); }, });

In this case, from() creates an Observable from the HTTP request that fetches user data. Then, the switchMap() operator transforms the stream, conditionally fetching posts if the user can post.

As you can see, data flows are handled as streams with declarative transformations, making it easier to manage asynchronous operations and their effects. The result is a more flexible and composable approach, especially useful for handling complex async workflows.

And that's it for our whistle-stop tour of reactive programming in Node!

Wrapping Up

In this post, we explored what reactive programming is and how it helps you implement event-driven and scalable Node.js applications.

You now know:

  • What reactive programming is
  • How it applies to Node.js
  • The benefits and drawbacks it brings to the table
  • The two main libraries to implement it in Node
  • How it compares to imperative programming in a simple Node async scenario

Thanks for reading!

Wondering what you can do next?

Finished this article? Here are a few more things you can do:

  • Share this article on social media
Antonello Zanini

Antonello Zanini

Guest author Antonello is a software engineer, but prefers to call himself a Technology Bishop. Spreading knowledge through writing is his mission.

All articles by Antonello Zanini

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps