Detecting the end of a fluent API chain in JavaScript
Published 5/14/2020
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()
into
const response = await new Client().get('/blogs').as('🦆')
// or
const response = await new Client().as('🦆').get('/blogs')
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
.
Check out my e-book!
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: { ... } })
}
}
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: { ... } }))
}
}
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.
Note: To be fair, there are some subtleties that we've missed with this simple approach. For example, calling "then" on a promise multiple times will only execute it the first time it gets called. You can achieve the same here by keeping a reference to the promise on the class instance when "then" is called. Subsequent calls would then simply return that reference.