icp-pkg-axios-cache
)
@zxventures/axios-cache (Table of Contents
Overview (or Why)
The goal of this library was to enable Axios along with a fast cache handling and to fix a few issues with the current library:
- Requests with status 200 but with errors node present.
- Lack of control over internal data management, for example, serialized errors.
- High level of complexity for use, during the development process.
- Simplify the clusters handling.
Quick start
Requirements
- Redis server must be installed and the service must be enabled.
- Supports Redis >= 2.6.12
- Node.js >= 12.22.0
Install
This package is scoped and is private by default.
Therefore, to use this package, it is necessary to include a file named
.npmrc
at the root of the project where this package will be used with the following content://registry.npmjs.org/:_authToken=PUT-THE-AUTHENTICATION-TOKEN-HERENOTE: The authentication token should be requested from the infrastructure team.
Using npm:
npm install @zxventures/axios-cache
Usage
In this package there are some clients, helpers, validations and others. All of them can be customized, extended or used individually.
However, the simplest way is to use the axiosCache
function.
This function allows us to create a new instance of axios with some extra features to work with redis cache and custom validation of responses with status 200 but with the error node present, usually in GraphQL responses.
In the same way that axios works, it can be a general instance with some specific configuration. Likewise, it can override the instance configuration when using the request function or some shortcut functions like .get
, .post
, etc.
By default, caching is disabled, but it can be enabled by setting some properties in the configuration.
The axiosCache
function receives as an optional parameter a configuration. This configuration has 2 optional properties. One of them is axiosConfig
as its name indicates is the axios configuration (in a normal way) with 2 additional properties: validateResponse
and cache
. Another property is redisClientConfig
and it is to set the custom redis client configuration.
const axiosCache = require('@zxventures/axios-cache');
const config = {
axiosConfig: { /* ... */ },
redisClientConfig: { /* ... */ }
};
const axiosInstance = axiosCache(config);
The custom redis client in this package uses the ioredis
library to work with redis.
Therefore, the complete configuration of redis is specified in the official documentation of the ioredis
library.
So then, the redisClientConfig
configuration has the following schema and default values:
const axiosCache = require('@zxventures/axios-cache');
const redisClientConfig = {
/*
* ioredis configuration to work with a standard redis server (no cluster)
* - https://github.com/luin/ioredis
* - https://luin.github.io/ioredis/classes/Redis.html
*
*/
redisConfig: {
lazyConnect: true,
connectTimeout: 10000,
enableOfflineQueue: false,
},
/*
* ioredis configuration to work with the Cluster approach
* - https://github.com/luin/ioredis#cluster
* - https://luin.github.io/ioredis/classes/Cluster.html
*
*/
clusterConfig: {
nodes: [],
options: { },
},
/*
* Additionals props
*
*/
// `maxAgeInMs` defines the default cache lifetime in milliseconds.
// - Type: Number
// - Default value is `Number.NaN`.
maxAgeInMs: Number.NaN,
};
const axiosInstance = axiosCache({ redisClientConfig });
To enable the cache, you only have to set the values of the properties in the cache property correctly.
const axiosCache = require('@zxventures/axios-cache');
const axiosConfig = {
cache: {
// `isActive` indicates whether or not to use the cache.
// - Type: Boolean
// - Default value is `false`.
isActive: true,
// `maxAgeInMs` defines the cache lifetime in milliseconds.
// this value must be greater than zero in order to be valid to use the cache.
// - Type: Number
// - Default value is `Number.NaN`.
maxAgeInMs: 1000 * 30, // e.g: 30 seconds
// `methodsToCache` to define which http methods will be valid to use the cache.
// - Type: Array of strings
// - Default value is `['get', 'post']`.
methodsToCache: ['get', 'post'],
// `configPathsToCreateKey` allows to define which configuration paths/properties
// will be used to create the cache key.
// If the value is not valid (e.g: empty array or a null/undefined) the cache key
// will be created using the full config.
// - Type: Array of strings (no deep path/property)
// - Default value is `['method', 'url', 'params', 'data']`.
configPathsToCreateKey: ['method', 'url', 'params', 'data'],
},
};
const axiosInstance = axiosCache({ axiosConfig });
Note that to enable and be able to use the cache, at least the following rules must be met:
- The value of
config.cache.isActive
must betrue
. - The value of
config.cache.maxAgeInMs
greater than zero. - One of the array values in
config.cache.methodsToCache
must match theconfig.method
specified in an axios request.
Examples
Below are some useful examples that will help you understand how this library works.
- Example #1
This is an example of a simple get request using cache.
Note how the flow is logged (console.log
) with some of the most relevant values in each step.
Also, when axiosCache
is used, the response schema has an additional property called isCachedData
.
const axiosCache = require('@zxventures/axios-cache');
// general variables
const traceTime = "Example #1 - Time taken";
// setup the configs
const axiosConfig = {
cache: {
isActive: true,
maxAgeInMs: 1000 * 60, // 1 min.
},
};
const redisClientConfig = {
redisConfig: {
host: process.env.AXIOS_CACHE_HOST,
port: process.env.AXIOS_CACHE_PORT,
},
};
// starts a timer that can be used to compute the duration of an operation
console.time(traceTime);
// create an axios instance using the configurations
const axiosInstance = axiosCache({ axiosConfig, redisClientConfig });
// make a get request
const response = await axiosInstance.get('https://httpstat.us/200');
// log the response
const { isCachedData, status, statusText, data } = response;
console.log('response', { isCachedData, status, statusText, data });
// stop the timer and print the result
console.timeEnd(traceTime);
/*
* Console output:
* ———————————————————————
* • [RedisClient] connect() { statusBeforeTryingToConnect: 'wait', isConnected: false }
* • [RedisClient] get() {
* cachedDataFound: false,
* key: '39141ac79410dd0cb7948b0d83d398c69f3247c96a3219d1a19ebe53a5fdafe6'
* }
* • [adapter] request(): axiosConfig {
* method: 'get',
* url: 'https://httpstat.us/200',
* data: undefined,
* headers: { Accept: 'application/json...' },
* adapter: [Function (anonymous)],
* validateResponse: [Function: validateResponse],
* cache: {
* isActive: true,
* maxAgeInMs: 60000,
* methodsToCache: [ 'get', 'post' ],
* configPathsToCreateKey: [ 'method', 'url', 'params', 'data' ]
* }
* ...
* }
* • [RedisClient] set() {
* ms: 60000,
* key: '39141ac79410dd0cb7948b0d83d398c69f3247c96a3219d1a19ebe53a5fdafe6'
* }
* • [RedisClient] disconnect() { statusBeforeTryingToDisconnect: 'ready', isConnected: true }
* response {
* isCachedData: false,
* status: 200,
* statusText: 'OK',
* data: { code: 200, description: 'OK' }
* }
* Example #1 - Time taken: 797.941ms
*
*/
If the above example is run again, the output looks like this:
/*
* Console output:
* ———————————————————————
* • [RedisClient] connect() { statusBeforeTryingToConnect: 'wait', isConnected: false }
* • [RedisClient] get() {
* cachedDataFound: true,
* key: '39141ac79410dd0cb7948b0d83d398c69f3247c96a3219d1a19ebe53a5fdafe6'
* }
* • [RedisClient] disconnect() { statusBeforeTryingToDisconnect: 'ready', isConnected: true }
* response {
* isCachedData: true,
* status: 200,
* statusText: 'OK',
* data: { code: 200, description: 'OK' }
* }
* Example #1 - Time taken: 49.522ms
*
*/
Note that the request has not been made and the set method is not called (RedisClient).
Also, now the value of the isCachedData
property is true and the time spent is much less.
- Example #2
This is a common example of a GraphQl endpoint request. In this case the GraphQl endpoint will response with a status 200 but with errors node present.
const axiosCache = require('@zxventures/axios-cache');
// setup the configs
const axiosConfig = {
cache: {
isActive: true,
maxAgeInMs: 1000 * 60, // 1 min.
},
};
const redisClientConfig = {
redisConfig: {
host: process.env.AXIOS_CACHE_HOST,
port: process.env.AXIOS_CACHE_PORT,
},
};
// create an axios instance using the configurations
const axiosInstance = axiosCache({ axiosConfig, redisClientConfig });
// create a request data
const sellerId = 'modeloramamxcentro';
const appToken = 'eyJ0eXAiOiJqd3QiLCJhbGciOiJ...';
const authorization = 'eyJraWQiOiJSd2tEbTZkRjBtQksrd...';
const requestData = {
method: 'POST',
url: 'https://dev-api.modelorama.online/graphql/gateway',
headers: {
appToken,
authorization,
},
data: {
variables: {
input: {
sellerId,
},
},
query: `
query minimumOrderValueQuery($input: MinimumOrderValueInput) {
minimumOrderValue(input: $input)
}
`,
},
};
try {
// make a request
const response = await axiosInstance.request(requestData);
// log the response
const { isCachedData, status, statusText, data } = response;
console.log('response', { isCachedData, status, statusText, data });
} catch (error) {
// log the error
const { message, code, name, innerError } = error;
console.error('error', { message, code, name, innerError });
}
/*
* Console output:
* ———————————————————————
* • [RedisClient] connect() { statusBeforeTryingToConnect: 'wait', isConnected: false }
* • [RedisClient] get() {
* cachedDataFound: false,
* key: '510bff30f5ac9d7d46104bddde88d3fe612769ceead537d56979bf637aee1a91'
* }
* • [adapter] request(): axiosConfig {
* method: 'post',
* url: 'https://dev-api.modelorama.online/graphql/gateway',
* data: '{"variables":{"input":{"sellerId":"modeloramamxcentro"}},"query":"..."}',
* headers: {
* Accept: 'application/json...',
* 'Content-Type': 'application/json',
* appToken: 'eyJ0eXAiOiJqd3QiLCJhbGciOiJ...',
* authorization: 'eyJraWQiOiJSd2tEbTZkRjBtQksrd...',
* },
* adapter: [Function (anonymous)],
* validateResponse: [Function: validateResponse],
* cache: {
* isActive: true,
* maxAgeInMs: 60000,
* methodsToCache: [ 'get', 'post' ],
* configPathsToCreateKey: [ 'method', 'url', 'params', 'data' ]
* }
* ...
* }
* • [RedisClient] disconnect() { statusBeforeTryingToDisconnect: 'ready', isConnected: true }
* error {
* message: 'Error trying to get the minimum order value',
* code: 'IOP-CHK-3001',
* name: 'AxiosCacheError',
* innerError: [AxiosError: Error trying to get the minimum order value] {
* ...
* },
* }
*
*/
Note that the request fails with an error of type AxiosCacheError
which inherits from CustomBaseError
(base error type from @zxventures/utils
library).
This is the error type used by this library to wrap the original error (AxiosError
) and has 3 main properties: message
, code
and innerError
.
Thanks to the extra property called validateResponse
the new flow (custom adapter) checks the response object with status 200 but with internal error nodes. Thus, it converts the response into an error using the information obtained from the internal error nodes.
To customize the validateResponse
function it is only necessary to override the original custom function This function receives a single argument called response
and no results are expected from it but you can alter the response object or throw an error.
It is important to note that at this point in the axios flow there are 2 things to keep in mind:
- the 'response.data' must be a String (serialized data).
- transform responses haven't been executed yet.
const axiosCache = require('@zxventures/axios-cache');
// setup the axios config
const axiosConfig = {
// `cache` defines how to work with axios-cache
// - Type: Object
cache: { /* ... */ },
// `validateResponse` defines a function that receives a single argument called `response`
// and no results are expected from it.
// - Type: Function (void)
// - Default value is custom validation to check the response object with
// status 200 but with internal error nodes.
validateResponse: (response) => { /* some custom validation */ }
};
// create an axios instance using the configuration
const axiosInstance = axiosCache({ axiosConfig });
// ...
- Example #3
This is an example where a base cache configuration is used in a global instance of axios. Then, when a request is made, the cache configuration is overwritten. It is useful when a specific use case requires some more specific configuration, for example: changing the lifetime of a cache or enable/disable the cache.
const axiosCache = require('@zxventures/axios-cache');
// setup the configs
const axiosConfig = {
cache: {
isActive: true,
maxAgeInMs: 1000 * 30, // 30 sec
},
};
const redisClientConfig = { /* ... */ };
/*
* `Client` with two particular methods (specific use cases)
* - getProductList(): change the lifetime of a cache
* - getAddressById(id): disable the cache
*
*/
class Client {
constructor() {
this.axiosInstance = axiosCache({ axiosConfig, redisClientConfig });
}
async getProductList() {
const config = {
cache: {
// change the value of `maxAgeInMs` to 5min instead of 30sec.
maxAgeInMs: (1000 * 60) * 5
},
method: 'POST',
headers: { /* appToken, authorization and others */ },
data: { /* some request data */ },
};
const response = await this.axiosInstance.request(config);
return response;
};
async getAddressById(id) {
const config = {
cache: {
// disable cache
isActive: false,
},
method: 'POST',
headers: { /* appToken, authorization and others */ },
data: { /* some request data */ },
};
const response = await this.axiosInstance.request(config);
return response;
};
// other methods using the standard instance of `axiosInstance`.
// ...
}
module.exports = Client;
- Example #4
This is an example using the redis cluster.
To work with the redis cluster it is only necessary to use the clusterConfig
property instead of the redisConfig
property and follow the instructions/guidelines of the official documentation of the ioredis
library to work with cluster.
const axiosCache = require('@zxventures/axios-cache');
// setup the configs
const axiosConfig = {
cache: {
isActive: true,
maxAgeInMs: 1000 * 60, // 1 min.
},
};
const redisClientConfig = {
// - https://github.com/luin/ioredis#cluster
clusterConfig: {
nodes: [{
port: 6380,
host: "127.0.0.1",
},
{
port: 6381,
host: "127.0.0.1",
}],
options: { /* ... */ },
},
};
// create an axios instance using the configurations
const axiosInstance = axiosCache({ axiosConfig, redisClientConfig });
// Now the `axiosInstance` is working with redis cluster.
// ...
Run unit test and code coverage
ADVICE: It is recommended to review and run the unit tests to better understand each of the utilities provided by this package.
Run
npm test
To run code coverage use
npm run coverage
Package Dependencies
In this section, include the packages used as dependencies to build this package.
Some examples can be:
- axios
- ioredis
- joi
- zlib
- @zxventures/utils (private package)