DEV Community

Cover image for Detecting the end of a fluent API chain in JavaScript
Michael Z
Michael Z

Posted on • Updated on • Originally published at michaelzanggl.com

Detecting the end of a fluent API chain in JavaScript

Originally posted at michaelzanggl.com.

Say, we are building a test library and want to create a fluent API for sending requests in integration tests.

We can turn something like this

// notice the explicit `.end()`
const response = await new Client().get('/blogs').as('🦆').end()
// or
const response2 = await new Client().as('🦆').get('/blogs').end()
Enter fullscreen mode Exit fullscreen mode

into

const response = await new Client().get('/blogs').as('🦆')
// or
const response = await new Client().as('🦆').get('/blogs')
Enter fullscreen mode Exit fullscreen mode

As you can see, we can chain the methods any way we want, but somehow end up with the response. All without an explicit method to end the chain like end().

How does it work? Well, it all lies in the little magic word await.

Unfortunately, that also means that detecting the end of a chain only works for asynchronous operations. I mean, theoretically, you could do it with synchronous code but you would have to use the await keyword, which might throw some off. Apart from this hack, there is no way in JavaScript currently to detect the end of a chain for synchronous operations.

So let's look at the first implementation with the explicit .end() method. Or jump straight to the solution.

Here is a possible API:

class Client {
  as(user) {
    this.user = user
    return this
  }

  get(endpoint) {
    this.endpoint = endpoint
    return this
  }

  async end() {
    return fetch(this.endpoint, { headers: { ... } })
  }
}
Enter fullscreen mode Exit fullscreen mode

solution

And here is the little trick to achieve it without an explicit end() method.

class Client {
  as(user) {
    this.user = user
    return this
  }

  get(endpoint) {
    this.endpoint = endpoint
    return this
  }

  async then(resolve, reject) {
    resolve(fetch(this.endpoint, { headers: { ... } }))
  }
}
Enter fullscreen mode Exit fullscreen mode

So all we needed to do was switching out end() with then() and instead of returning the result, we pass it through the resolve callback.

If you have worked with promises, you are probably already familiar with the word then. And if you ever used new Promise((resolve, reject) => ... this syntax will look weirdly familiar.

Congratulations. You have just successfully duck-typed A+ promises.

A promise is nothing more than a thenable (an object with a then method), which conforms to the specs. And await is simply a wrapper around promises to provide cleaner, concise syntax.

So in summary, to achieve an async fluent API, all you need to do is define a then method which either resolves or rejects any value through the two given arguments.

Discussion (0)