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!

Imagine monitoring actually built for developers

Billboard image

Join Vercel, CrowdStrike, and thousands of other teams that trust Checkly to streamline monitor creation and configuration with Monitoring as Code.

Start Monitoring

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

Heroku

This site is powered by Heroku

Heroku was created by developers, for developers. Get started today and find out why Heroku has been the platform of choice for brands like DEV for over a decade.

Sign Up

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay