Fetch the way you desire
- Drop-in fetch replacement
- No fetch quirks
- Plugins and middlewares
- Use out of the box
- Pick the features you need
- Customize anything
import {fetish, baseUrl, defaultHeaders} from 'fetish';
const client = fetish
.with(baseUrl('http://example.org/api/v2'))
.with(defaultHeaders({
'X-API-Key': 'secret'
}));
client.post('/posts', {
body: {
title: 'me',
body: 'likey'
}
});
npm i fetish
Or, if you prefer, with Yarn:
yarn add fetish
Boxed fetish pulls in isomorphic-fetch and es6-promise polyfills, but of course you can override both of those, use fetish-peer
(with peer dependency) or even pick and choose features you want with fetish-nude
.
import {fetish, customPromise} from 'fetish';
import Promise from 'bluebird';
const client = fetish.with(customPromise(Promise));
client('/too-fast').delay(100); // use bluebird's promise methods
import {fetish, customFetch} from 'fetish';
import fetch from 'fetch-ie8';
const client = fetish.with(customFetch(fetch));
Here is a glimpse of how fetish is implemented. It starts with fetish-nude which is a simple wrapper around fetch with only one extra method called with
used to add plugins. Plugins, which are just functions from one fetish to a better fetish, are then added to it.
Let's build a barebone http client with only one added feature: it will JSON.stringify
the request body.
npm i fetish-nude fetish-plugin-fetish-plugin-serialize-body-to-json
exports.fetish = fetishNude
.with(serializeBodyToJson);
If none of the existing plugins suits your fancy, creating a custom plugin is trivial.
If you don't want to use isomorphic-fetch
or es6-promise
polyfills that come as dependencies with the fetish
package, use fetish-peer
instead.
Obviously, if you choose fetish-peer
package, it is expected that your runtime has native fetch and Promise support or that you install fetch and Promise polyfills of your choice separately.
Plugins are middlewares. Whoa, right?
The complete list can be found in packages directory and on npm.
fetish.fetch
tries to be as fetch
-compatible as it can.
If you are going full custom and need this, use fetish-plugin-fetch-drop-in
.
What quirks? Fetch is awesome, right? Almost right.
While Promise is multicast, Response is not. You can pass a promise to as many consumers as you like and they can call promise.then
multiple times, but while you can pass a response to many consumers, only the first call of response.json()
can ever be successful. There are good reasons for that, but there are also good reasons not to want that.
This is precisely what fetish-plugin-multicast-response
fixes by monkey-patching all stream-reading methods. Unfortunately, if you try to read a response body in multiple ways (like doing r.json()
and then also r.text()
) you'd still get an error.
Fetish plugins are functions from one fetish to a better fetish. And fetish is a function from request options to a Promise of a Response. This gives plugins the power to change anything about fetish: options, response or the fetish function itself.
Approximate type signature:
type Options = { /* url, body, query, headers, etc. */ };
type Fetish = Options => Promise<Response>;
type Plugin = Fetish => Fetish;
This plugin changes nothing about the fetish, but hopefully serves as a good first example.
const identity = nextFetish => options => nextFetish(options).then(response => response);
const client = fetish.with(identity);
You can see from this example and the types above that plugins can easily implement middlewares in this setting.
Say you want to add a custom header to every request. You could use fetch-plugin-default-options
, but we will do it the hard way instead.
import {defaultsDeep} from 'lodash/fp';
// Easy mode
const myHeaders = nextFetish => options => {
return nextFetish(defaultsDeep(options, {
headers: {
'X-Hello': 'World'
}
}));
};
// A bit more generic
const defaultHeaders = headers => nextFetish => options => nextFetish(defaultsDeep(options {headers}));
const client = fetish
.with(myHeaders)
.with(defaultHeaders({
'X-Beep': 'Boop'
}));
Let's make a plugin that lets one parse a response into an immutable.js object. The real implementation is called fetish-plugin-immutable-response
.
We are going to add response.immutable()
method as a simple wrapper around response.json()
.
Generally, actually modifying responses is a bit tricky because of the way Response is defined. It's immutable and it does not define any "copying setter" methods. One could use new Response()
, but in reality I would prefer monkey-patching the response anyway (sorry).
import {fromJS} from 'immutable';
const immutable = nextFetish => options => nextFetish(options).then(response => {
response.immutable = () => response.json().then(fromJS);
return response;
});
const client = fetish.with(immutable);
// Somewhere else:
const treasure = await client('/treasure/1').then(r => r.immutable());
Note that you can just as easily throw an error from the .then
callback of your plugin. This way you can turn erroneous responses into exceptions, and that's actually what fetish-plugin-throw-http-errors
does. Or use .catch
in a simillar way, maybe you want to recover from errors, whatever, you got the point.
How do you add custom methods like fetch.post
though?
const postMethod = nextFetish => Object.assign(nextFetish, {
post: function (options) {
// `this` here will be the final fetish someone calls like `fetish.post(...)`
return this(Object.assign({}, options, {
method: 'POST'
}));
}
});
const client = fetish.with(postMethod);
// Later:
const response = await client.post({
url: '/posts/',
body: { i: 'post' }
});
But of cource this basic stuff is already impletemented in fetish-plugin-http-methods
. Hope you come up with something more exciting!
Consider contributing you plugins and stuff to this repo.
If you deicide to publish your plugins to npm separately from this repo, please try to be careful about the "fetish-*" namespace-like thingy.