Quick article to give away my custom Shopify Admin REST API client (based on Axios), that integrates a retry strategy along with a pagination sugar to overcome the very restrictive rate limit policy of Shopify. Note that it also removes the need to wraps the data sent under the resource name like { customer: { data } }
.
Usage
const shopifyAdmin = require('./shopifyAdmin');
const myShop = shopifyAdmin({
shopName: 'your-shop-name',
version: '2021-01',
apiKey: '**********',
password: '**********',
});
myShop('POST /customers', { email: 'you@example.com' });
myShop('GET /customers/search', { query: 'email:"you@example.com"' });
myShop('GET[ALL] /orders'); // paginate and return all
myShop('GET[500] /orders'); // paginate and return first 500
Consider the performance drag of pagination and retry (whenever the API returns a 429). The retry strategy adds a 1 second delay before each call. Better use this in async jobs (like with Bull) whenever possible.
Client source code
const axios = require('axios');
const parseHeaderLink = require('parse-link-header');
/**
* Custom Axios client for Shopify Admin.
*
* Implements pagination, retry strategy, and call frequency control as a workaround for Shopify Admin API call limits.
*
* @arg {Object} credentials Shopify Admin API credentials.
* @arg {string} credentials.shopName Shopify shop name
* @arg {string} credentials.version Shopify Admin API version
* @arg {string} credentials.apiKey Shopify Admin API key
* @arg {string} credentials.password Shopify Admin API password
* @return {Function} Axios request wrapper.
*/
export default ({ shopName, version, apiKey, password }) => {
if (!shopName || !version || !apiKey || !password) {
throw Error('Shopify Admin init: missing credentials.');
}
const baseURL = `https://${shopName}.myshopify.com/admin/api/${version}`;
const token = Buffer.from(`${apiKey}:${password}`).toString('base64');
const client = axios.create({
baseURL,
headers: {
Authorization: `Basic ${token}`,
},
});
/**
* Retry if too many requests.
* @arg {Function} fn Function to execute.
* @return {Promise<Object[]>} Axios response Promise.
*/
const retry = async (fn) => new Promise((resolve, reject) => {
setTimeout(() => {
fn().then(resolve).catch((err) => {
if (err.message.includes('429')) {
retry(fn).then(resolve).catch(reject);
} else {
reject(err);
}
});
}, 1000);
});
/**
* Returns concatenated results after running pagination.
* @arg {Object} config Axios request parameters
* @arg {number} limit Number of results to return ('undefined' returns all)
* @return {Promise<Object[]>} Axios response Promise.
*/
const paginate = async (config, limit) => {
let output = [];
do {
const res = await retry(() => client.request(config));
output = output.concat(Object.values(res.data)[0]);
const links = parseHeaderLink(res.headers.link);
config.params = links && links.next ? {
limit: links.next.limit,
page_info: links.next.page_info,
} : undefined;
} while (config.params && (!limit || output.length <= limit));
return output.slice(0, limit);
};
/**
* Axios request wrapper.
* @arg {string} action Method + URL combined (e.g. "GET /orders").
* @arg {Object} [data] Body data.
* @arg {Object} [params] Query parameters.
* @return {Promise} Axios response Promise.
* @example
* // Get products
* shopifyAdmin('/products')
* shopifyAdmin('GET /products')
* @example
* // Add customer
* shopifyAdmin('POST /customers', { email: 'you@example.com' })
* @example
* // Search customers
* shopifyAdmin('GET /customers/search', { query: 'email:"you@example.com"' })
* @example
* // Get all orders (uses pagination)
* shopifyAdmin('GET[ALL] /orders')
* @example
* // Get 500 orders (uses pagination)
* shopifyAdmin('GET[500] /orders')
*/
return (action, data = {}, params = {}) => {
if (!action) {
throw Error('Shopify Admin client: missing request URL.');
}
const [method, url] = action.trim().split(/\s+/);
const config = { url: `${url || method}.json` };
const superGetRegexp = /get\[(all|[0-9]+)\]/i;
config.method = (url ? method.replace(superGetRegexp, 'GET') : 'GET').toUpperCase();
if (['DELETE', 'GET'].includes(config.method)) {
// pass data as query string
config.params = {
...data,
...params,
};
} else {
// wrap data under resource (ex: `{customer: data}`)
const resource = url.replace(/^\/?(?:[a-z_0-9]+\/)*([a-z_]+)s(?:\/[0-9]+)*$/, '$1');
config.data = { [resource]: data };
config.params = params;
}
const paginated = method.match(superGetRegexp);
return paginated ?
paginate(config, parseInt(paginated[1], 10) || undefined) :
retry(() => client.request(config).then((res) => Object.values(res.data)[0]));
};
};
Hope this helps!
Top comments (1)
I edited the code multiple times to make it more robust, there were some bugs going on because I translated back from my Typescript instance and now I've tested this JS one and fixed it. If you had copied this code before Feb 24, you need to update :).