try-catch-FAIL

Failure is inevitable

NAVIGATION - SEARCH

Practical Promises in JavaScript - Leveraging Promise Chaining to Achieve Encapsulation

Welcome to part 5 of my Practical Promises series! Today, we're going to use what we've learned to build a better client-side API for a web API.

If you are just joining us, here is what you missed:

In part 1, we talked about what promises are and what they can be used for.

In part 2, we started looking at how we can create promises.

Then in part 3, we saw how each call to then actually makes a new promise, and that those promises can be chained together.

In part 4, we learned how to combine promise chaining with the creation of new promises in order to simplify complex async workflows.

The Challenge

Let's say we have a typical REST Web API for interacting with Widgets. You can get widgets, create new ones, etc etc. A sample GET request might look like this:

GET http://our.api/Widgets

[
    {"id": "1", "name": "Widget 1", ...},
    {"id": "2", "name": "Widget 2", ...},
    {"id": "3", "name": "Widget 3", ...},
    ...
]

Let's also pretend that we want to provide developers with a nice client-side API they can use to interact with this web API.

One option would be to create a very low-level API that does little more than encapsulate the URLs for our API.

Our raw code HTTP request code might look something like this:

function getWidget(id) {
    //Note: you don't really need a variable here, but I wanted to make it obvious what we're handing back.
    const promise = request(`http://our/api/widget/${id}`);

    return promise;
}

The request function actually returns a response object, which might look something like this:

{
    "statusCode": "200",
    "data": "{\"id\":\"1\", \"name\":\"widget 1\"}"
}

That means that anyone that calls our getWidget function will need to unwrap the response to actually use it, like so:

getWidget(1).then(response => {
   $ctrl.widget = JSON.parse(response.data); 
});

That means our callers are now coupled to the underlying request function and how it returns data. That's not ideal.

Using Promises to Unwrap Objects

We can use what we've learned about promise chaining to encapsulate request logic and expose a simpler result.

Instead of returning the response object directly, we'll expose an API that will return just the widget to callers. We can leverage promise chaining to do exactly that:

function getWidget(id) {
    const promise = request(`http://our/api/widget/${id}`);
    
    return promise.then(response => JSON.parse(response.data))
}

Remember: Calling promise.then actually creates a new promise, and that promise will be resolved with whatever we return from our then callback. In this case, that is going to be the result of JSON.parse.

Now anyone that uses our getWidget function will receive the actual widget, and not the underlying response:

getWidget(1).then(widget => {
   $ctrl.widget = widget;
});

This encapsulation will also simplify everyone's lives when its time to put specs around components that utilize our API. Instead of mocking out a complex response object, mocks can be created to return our Widget object directly.

Up Next

We're almost finished with this run through promises, but there's one last thing I want to show you: how to use the new ES7 async/await features to simplify things even further! Stay tuned!

About Matt Honeycutt...

Matt Honeycutt is a software architect specializing in ASP.NET web applications, particularly ASP.NET MVC. He has over a decade of experience in building (and testing!) web applications. He’s an avid practitioner of Test-Driven Development, creating both the SpecsFor and SpecsFor.Mvc frameworks.

He's also an author for Pluralsight, where he publishes courses on everything from web applications to testing!

blog comments powered by Disqus