DEV Community

Nicolas Torres
Nicolas Torres

Posted on • Edited on

2

Bypassing Shopify Admin REST API limitations with a custom JS client

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
Enter fullscreen mode Exit fullscreen mode

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]));
  };
};
Enter fullscreen mode Exit fullscreen mode

Hope this helps!

SurveyJS custom survey software

Build Your Own Forms without Manual Coding

SurveyJS UI libraries let you build a JSON-based form management system that integrates with any backend, giving you full control over your data with no user limits. Includes support for custom question types, skip logic, an integrated CSS editor, PDF export, real-time analytics, and more.

Learn more

Top comments (1)

Collapse
 
noclat profile image
Nicolas Torres • Edited

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 :).

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay