Http-client with a well-defined interface, swappable implementation, and in-built testing-tools.
#### 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'
}
});
`HTTPClient
You may also instantiate a new instance of 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);
});
`'GET'API:
$3
|property|type|required|default|
|-|-|:-:|-|
|url|String|✓||
|method|String|||{'Accept': 'application/json', 'Content-Type': 'application/json'}
|body|Object|||
|headers|Object|||0
|timeout|Number|||
|withCredentials|Boolean|||
#### NOTE: additional options supplied in the request config are not supported by http-client, but are passed through to the makeRequest implementation.
. 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|
$3
Currently, a request may be canceled using its cancel method:
`
const request = api.get('/foo');
request.cancel();
`Advanced usage
$3
$3
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
$3
#### 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:
1. Are cached requests subject to new request` interceptors which are added after the requests have been cached?