Exposing a Promise-Based API from a Reactive Core (v3.0 Journal)

The whole team here is going through very rewarding times lately.

Since we’ve started working on our new shopping cart’s frontend stack, we’ve had the chance to dive deeper into some awesome modern techs.

This rewrite is entirely done in TypeScript, decoupling the theming from a freshly baked SDK.

Redux is also a central piece of this stack. My good friend Jean-Seb already covered why we are using it—today, let’s move a bit more under the hood.

I want to use our experience as an example of how to use Redux middleware. More precisely, I’ll explain how we exposed a promise-based API from a reactive core. To do so, I’ll:

  • Define what exactly Redux middleware is
  • Expose more details about our SDK
  • Explain how we ended up exposing a promise-based API from it
  • Show how we leverage Redux middleware capabilities

This article is the second chapter of our v3.0 Journal where we reveal interesting parts of our shopping cart’s rewrite. To read the entire thing:

What is Redux middleware?

Generally, middleware refers to bits of code that sit between a source and a destination, executing logic and potentially altering a given object along the way. If you worked with a web application framework in the past (like Express or Koa), chances are you've dealt with middleware.

To describe Redux’s middleware let’s refer to its official documentation:

Redux middleware solves different problems than Express or Koa middleware but in a conceptually similar way. It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

Reading the docs, you might think Redux middleware is a big monolithic piece of the puzzle. Although it technically is, think of it more as a composition of multiple middleware functions.


The basics of Redux middleware shown in 5 minutes by LearnCode.academy

Redux middlewares are used for a number of reasons: logging actions, crash reporting, routing, etc. For our specific use case, as for many others, we use one to talk to an asynchronous API. We chose redux-observable to do that.

One thing you have to know about Redux is that its reducers must be synchronous. So, atomic operations as a whole must be represented by more than one action, since they happen asynchronously. You can see how it can become cumbersome for people not accustomed to using reactive patterns.

That’s why we had to think of a way to abstract all this process away from developers using our library so it could remain accessible to the vast majority of them.

I think the best way to understand how it all works is by learning from real-life examples. Let’s waste no more time and get into it!

Our Redux middleware example (or exposing a promise-based API from a reactive core)

redux-middleware-example

Important definitions

Let's shed some light on a few more concepts we'll play with:

  • Reactivity is a paradigm where the execution of code occurs as a reaction to some other side-effect.
  • A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.
  • The actions pipeline is the term I use to designate the flow of Redux actions down the middleware stack to the reducers and back up the middleware stack again.

I also recommend reading about Redux's data flow to help you get a grasp on what follows in the rest of this post.

Challenge: using Redux without forcing it upon users

Some developers don’t know Redux. Some developers don’t even have a clue what reactivity means. Nevertheless, they should still be able to use our product and be able to customize their Snipcart integration.

So, it would be a shame having to ask them to dispatch actions to our Redux store, right?

what-is-redux

Say you want to add an item to your cart. You expect to call something like sdk.addItem(myNewItem) that'll return a promise. That's the standard way of doing things.

Using Redux to develop the internal plumbing of our SDK gives us flexibility in covering complex use case like debouncing and throttling.

However, the flipside of using Redux is that we do not accommodate the simple promise pattern out of the box. Actions get dispatched. Remote calls happen within redux-observable epics, which in turn dispatch new actions. The global reactive state gets mutated along the way, and all of this is entirely asynchronous.

No need to say we had to hide away this complexity as much as we could. Our goals were twofold:

  • Developers must be able to execute an atomic operation such as addItem the “classic” way, i.e., calling methods that return promises
  • The adapter that makes the operation happen should be integrated in the most unobtrusive way possible for our team

Challenge accepted: how we achieved it

Since we're big fans of the DRY (Don’t Repeat Yourself) principle, we wanted to put something in place that would be cross-cutting over all of the actions being dispatched. Something that would hook itself into the actions pipeline and manage the whole promises thing automatically.

This is starting to resemble a middleware, right?

Atomic operation == more than one action

The first thing to understand is that operations span over more than one action dispatch. Let's get back to the addItem I mentioned earlier. Since it's an async remote call to the Snipacrt API, we apply the async action pattern using redux-observable.

The operation is split into distinct actions: ITEM_ADD, ITEM_ADDED , and ITEM_ADD_FAILED

Wrapping our addItem scenario then takes one of these two forms:

  • New promise created with ITEM_ADD, and resolved with ITEM_ADDED
  • New promise created with ITEM_ADD, and rejected with ITEM_ADD_FAILED

Adding context to actions

First, we need a way to tell our middleware what actions should be wrapped, and what actions should just fly by, untouched. We'll add a source property.

We also need to indicate which subsequent action(s) will resolve the promise and which one(s) will reject it. That's what resolvesWith and rejectsWith arrays are for.

So now, this simple SDK call:

    sdk.addItem({
        id: 'eggnog',
        name: 'Eggnogg carton - 2L',
        price: 6.99
    })

Will be dispatched like this internally:

    store.dispatch({
        type: 'ITEM_ADD',
        source: 'SDK'
        resolvesWith: ['ITEM_ADDED'],
        rejectsWith: ['ITEM_ADD_FAILED']
        payload: {
            id: 'eggnog',
            name: 'Eggnogg carton - 2L',
            price: 6.99
        }
    })

Leveraging the Redux middleware

We call it PromiseWrapperMiddleware. It has the responsibility of tracking promises, which happens in two stages:

1. Wrap

When an action with source:'SDK' gets dispatched, our middleware:

  • Adds an identifier property to the action
  • Creates a new promise
  • Saves the promise resolution context (callbacks and resolution rules) in a local cache with the identifier value as the key.
  • Relays to the next middleware
  • Returns the promise to the upstream caller.

The ID we add is what ties the whole operation lifecycle together. It'll be carried over to each subsequent action dispatched as a result of the initial one.

2. Unwrap

When an action with an identifier gets dispatched, that means it's part of a wrapped operation. Our middleware then:

  • Relays to the next middleware. This is important to do it first because we want the action to mutate our store's state before resolving the promise, so everything remains consistent
  • Retrieves the promise resolution context from the local cache using the ID
  • Resolves/rejects the promise if the action dispatched matches any of the resolvesWith/rejectsWith values
  • Clears the entry from the local cache
  • Returns to the upstream caller

Wrapping up (pun intended)

More of a visual person? Me too. Here's what the big picture looks like.

redux-middleware-api-call

The above shows the process of adding an item, happening in 2 steps:

  1. An addItem(...) call is made from the public layer of the SDK. This call dispatches the ADD_ITEM action. As it flows through the pipeline, the action gets a promise associated with it by our middleware (blue). It then continues its course down to the Redux store’s state. When the action heads back upstream, it hits the redux-observable middleware (purple), where a remote call to the backend API is fired.
  2. When the asynchronous API call is completed, depending on the outcome, an action of type ITEM_ADDED or ITEM_ADD_FAILED is dispatched. When this action reaches our middleware (blue), the promise associated with the atomic addItem operation gets resolved/rejected.

Closing thoughts & extra resources

Adopting this approach makes the developer experience as smooth as possible for our SDK users, but at the same time allows us to leverage the incredible power of Redux. The best of both worlds, as they say.

I hope this resource gives you a better grasp of what middleware is and what it can be used for. Needless to say, it’s a key piece of the puzzle for us. Now, I know it’s a particular use case—there are many more ways of leveraging Redux middleware. To learn more, I recommend these resources:

If you’re interested in the concepts covered here—SDKs, Redux, TypeScript, Vue.js— I encourage you to stay in touch with our blog. Our v3.0 Journal series will be back in early 2019 with more experiments around these great tools.

As always, we’re very open to feedback. Let us know what you think in the comments!


If you've enjoyed this post, please take a second to share it on Twitter.

Suggested posts: