@nuskin/http-client
v3.0.2
Published
Http-client with a well-defined interface, swappable implementation, and in-built testing-tools.
Downloads
627
Keywords
Readme
HTTP-client
Make ajax calls through a unified interface.
Features
- Easy to use
- Promise-based
- Clean interface
- Mocking built in
- Swappable implementation
- Interceptors
Installation:
$ npm i @nuskin/http-client
or yarn add @nuskin/http-client
Usage:
The default import provides a pseudo-instance of HTTPClient
with the convenience of being a function which behaves much in the same way as $.ajax
, axios
, or fetch
. Under the hood it is delegating to its request
method.
import api from '@nuskin/http-client';
api({
url: '/foo',
method: 'GET'
}).then(({ body }) => {
console.log('body is JSON');
}).catch(error => {
console.error(error);
console.log(error.response.status);
});
api.post({
url: '/foo',
body: {
foo: 'bar'
}
});
You may also instantiate a new instance of HTTPClient
via the create
factory function.
Instances have isolated interceptors (except of course for the global interceptors), and may be provided a base request-configuration which will get extended via shallow-merge with the configuration settings of each request being made. This allows you to provide any common settings which a group of calls may need to share.
import { create } from '@nuskin/http-client';
const api = create({
baseConfig: {
headers: {
foo: 'bar'
}
}
});
// All calls will be sent with a 'foo: bar' header
api({ url: '/foo', method: 'GET' });
api.get({ url: '/foo' });
Interceptors:
Interceptors are functions which may run either globally or on an instance-level, and "intercept" an http call at different periods of the request. The stages are:
- request
- success
- error
They may be thought of as "pipes" which receive input, and whose output will be used as the argument for the next interceptor or for the final request consumers in the application-code. To add a global interceptor, explicit methods to do so are available as named exports from the http-client library. Note that global interceptors are run before http-client interceptors, even if added after.
import api, { addGlobalInterceptor, removeGlobalInterceptor } from '@nuskin/http-client';
api.addInterceptor({
request (config) {
return Object.assign(config, {
headers: {
...config.headers,
'Client-Id': '12345'
}
}):
},
success (response) {
return Object.assign(response, {
customProp: 'applied-last'
});
}
});
addGlobalInterceptor({
success (response) {
return Object.assign(response, {
customProp: 'applied-first'
});
}
});
const request = api.get('/foo');
request.headers['Client-Id'] === '12345';
request.then(response => {
response.customProp === 'applied-last';
});
api.addInterceptor({
error (error) {
return 'foo';
}
});
api.get('/foo-error')
.then(response => {
response === 'foo';
})
.catch(error => {
// This won't get called since we're changing the error response into a plain object.
});
// To remove an interceptor, a reference to the interceptor added must be used.
Testing:
Note: The following example uses jest, but mocking http-client is test-runner agnostic.
import { mock, mockNetworkError, mockTimeout } from '@nuskin/http-client';
import { showButtonOnSuccessfulAjaxCall } from '../myModule';
mock({
'/get-button-contents': {
GET: {
status: 200,
body: {
bar: 'baz'
}
}
}
});
...
it('Should succeed when calling foo, with data', async () => {
await showButtonOnSuccessfulAjaxCall();
expect(buttonElem.exists()).toBe(true);
});
API:
The request config
|property|type|required|default|
|-|-|:-:|-|
|url|String|✓||
|method|String||'GET'
|
|body|Object|||
|headers|Object||{'Accept': 'application/json', 'Content-Type': 'application/json'}
|
|timeout|Number||0
|
|withCredentials|Boolean|||
NOTE: additional options supplied in the request config are not supported by http-client, but are passed through to the makeRequest
implementation.
Resolved object
|property|type|info| |-|-|-| |status|Number|| |body|Object|| |config|Object|The request config used to make the call. |headers|Object||
Rejected object
The rejected object is an instance of a HTTPError
. Errors resulting from a 404
, 500
, 403
will be instances of NotFoundError
, InternalServerError
, and ForbiddenError
, respectively. The names of these Error
sub-classes can be found here. To use the error classes, import them like so: import { NetworkError, NotFoundError } from '@nuskin/http-client';
|property|type|info| |-|-|-| |message|String|An error description| |response|Object|The server response| |config|Object|The config used to make the request|
Canceling
Currently, a request may be canceled using its cancel
method:
const request = api.get('/foo');
request.cancel();
Advanced usage
HTTP-client's aim is to provide a well-defined interface over-top of an implementation which actually performs the request.
To use your own implementation, use the implementation
option when using the create
method.
To implement the http-client "back-end", all that is currently needed is to implement a makeRequest
method. Here is an example which swaps the default implementation with a mock. This is similar to axios's adapters.
import { create } from '@nuskin/http-client';
const api = create({
implementation: {
makeRequest (config) {
if (config.url === '/successMe') {
return Promise.resolve({
body: 'yay!',
status: 200
});
} else {
return Promise.reject({
message: 'An error occurred',
isError: true,
response: {
status: 500
}
});
}
}
}
});
api.get('/successMe');
Testing
Testing of http-client is done through the provided testing-tools
. It has a simple interface for setting responses. Calling mock
multiple times will override the previous set of responses.
import { mock } from '@nuskin/http-client';
mock({
'/test': {
GET: {
status: 200,
body: {
foo: 'bar!'
},
headers: {
'X-FOO': 'BAR!'
}
},
POST: {
status: 500
}
},
'/network-error': {
GET: false,
}
});
TODO
Caching
One of the main feature requests is to have a caching system handled by http-client. It was originally built with a semi-naive cache which handled cache expiration and pass-through calls, but lacked design and was implemented before interceptors which caused unintended complications in the code.
Some questions to consider:
- Are cached requests subject to new
request
interceptors which are added after the requests have been cached? - Use Cache-Control request headers to implement caching?
- What should the default expiration be?
- How would the implementation differ if designing for a SPA or designing for a multi-page app?