Batching with API Mesh for Adobe Developer App Builder

Batching allows you to combine a group of requests into a single request, turning multiple queries into a single one. Compared to sending multiple queries simultaneously, batched requests result in better response times. They also avoid issues with rate-limiting.

data-variant=info
data-slots=text
Batching is only possible if the APIs included in your mesh support batching.

The following graphics depict the difference between queries with batched and unbatched calls:

Unbatched

If your sources do not support batching, each query runs separately.

unbatched

Batched

If your sources support batching, and you batch with declarative or programmatic resolvers, multiple queries combine to form a single request.

batched

The n+1 problem

The n+1 problem occurs when you request multiple pieces of information that cause the system to make multiple (n) queries to a source instead of using a single query. Since each query takes approximately the same amount of time, processing many queries can lead to degraded performance. In this example, a Reviews API contains reviews of your products by SKU. Without batching, you would need to query each SKU individually to return the corresponding reviews.

Example (without batching)

Consider a scenario where you are using the following mesh, where the Reviews source is a third-party API that contains reviews for your products by SKU. Each review consists of a review, customer_name, and rating field.

{
  "meshConfig": {
    "sources": [
      {
        "name": "Products",
        "handler": {
          "graphql": {
            "endpoint": "https://venia.magento.com/graphql"
          }
        }
      },
      {
        "name": "Reviews",
        "handler": {
          "graphql": {
            "endpoint": "<Reviews_API_URL>",
            "useGETForQueries": true
          }
        }
      }
    ],
    "additionalTypeDefs": "extend type ConfigurableProduct { customer_reviews: [productReviewslist]} ",
    "additionalResolvers": [
      {
        "targetFieldName": "customer_reviews",
        "targetTypeName": "ConfigurableProduct",
        "sourceName": "Reviews",
        "sourceTypeName": "Query",
        "sourceFieldName": "productsReviews",
        "requiredSelectionSet": "{ sku }",
        "sourceArgs": {
          "sku": "{root.sku}"
        }
      }
    ],
    "responseConfig": {
      "includeHTTPDetails": true
    }
  }
}
data-variant=info
data-slots=text
Use "includeHTTPDetails": true to see response details that indicate how many calls your mesh made to each source.

The custom resolver extends the type ConfigurableProduct with a new customer_reviews field, which allows nesting review fields inside of queries against the Venia source. The resolver is composed of the following components:

data-variant=info
data-slots=text
Use "includeHTTPDetails": true to see response details that indicate how many calls your mesh made to each source.

The following query causes multiple calls to the Reviews API:

{
  products(filter: { sku: { in: ["VD03", "VT12"] } }) {
    items {
      ... on ConfigurableProduct {
        sku
        name
        customer_reviews {
          sku
          reviews {
            review
            customer_name
            rating
          }
        }
        __typename
      }
    }
  }
}

Batching with declarative resolvers

The following example explains how to use batching inside your mesh configuration file by using declarative resolvers.

The Reviews source takes an array of product SKUs and returns an array of reviews for each SKU. To make a single network request to the Reviews source for multiple SKUs, add keysArg and keyField to your mesh.

data-variant=info
data-slots=text
Request batching using API Mesh requires a source endpoint capable of processing an array of values.
{
  "meshConfig": {
    "sources": [
      {
        "name": "Products",
        "handler": {
          "graphql": {
            "endpoint": " https://venia.magento.com/graphql"
          }
        }
      },
      {
        "name": "Reviews",
        "handler": {
          "graphql": {
            "endpoint": "<Reviews_API_URL>",
            "useGETForQueries": true
          }
        }
      }
    ],
    "additionalTypeDefs": "extend type ConfigurableProduct { customer_reviews: productReviewslist} ",
    "additionalResolvers": [
      {
        "targetFieldName": "customer_reviews",
        "targetTypeName": "ConfigurableProduct",
        "sourceName": "Reviews",
        "sourceTypeName": "Query",
        "sourceFieldName": "productsReviews",
        "keysArg": "sku",
        "keyField": "sku"
      }
    ],
    "responseConfig": {
      "includeHTTPDetails": true
    }
  }
}

requiredSelectionSet and sourceArgs are replaced with keysarg and keyField:

With the updated mesh, using the previous query returns the same information, but only makes one call to the Reviews source for multiple SKUs.

Batching with programmatic resolvers

The following example explains how to use batching inside your mesh configuration file by using programmatic resolvers.

In the following example, args.skus creates an array of SKUs to query instead of querying each SKU individually. The valuesFromResults object is optional and allows you to filter, sort, and transform your results.

In the following example, you would create your mesh configuration file (mesh.json) and the referenced JavaScript file (resolver.js) in the same directory.

data-variant=info
data-slots=text
The resolvers.js file contains similar logic to the additionalResolvers.js file in Programmatic Resolvers, but adds batching and logging.
data-slots=heading, code
data-repeat=3
data-languages=json, javascript, graphql

mesh.json

{
  "meshConfig": {
    "sources": [
      {
        "name": "AdobeCommerce",
        "handler": {
          "graphql": {
            "endpoint": "https://venia.magento.com/graphql"
          }
        }
      },
      {
        "name": "DiscountsAPI",
        "handler": {
          "JsonSchema": {
            "baseUrl": "https://random-discounts-generator.apimesh-adobe-test.workers.dev",
            "operations": [
              {
                "type": "Query",
                "field": "discounts",
                "path": "/getDiscounts?skus={args.skus}",
                "method": "GET",
                "requestSample": "https://random-discounts-generator.apimesh-adobe-test.workers.dev/getDiscounts?skus=[%27abc%27,%20%27xyz%27]",
                "responseSample": "https://random-discounts-generator.apimesh-adobe-test.workers.dev/getDiscounts?skus=[%27abc%27,%20%27xyz%27]",
                "argTypeMap": {
                  "skus": {
                    "type": "array"
                  }
                }
              }
            ]
          }
        }
      }
    ],
    "additionalResolvers": ["./resolvers.js"]
  }
}

resolver.js

module.exports = {
  resolvers: {
    ConfigurableProduct: {
      special_price: {
        selectionSet:
          "{ name price_range { maximum_price { final_price { value } } } }",
        resolve: (root, args, context, info) => {
          return context.DiscountsAPI.Query.discounts({
            root,
            key: root.sku,
            argsFromKeys: (skus) => ({ skus }),
            valuesFromResults: (results) =>
              results.map(({ discount }) => discount),
            context,
            info,
            selectionSet: "{ sku discount }",
          })
            .then((discount) => {
              let max = 0;

              try {
                max = root.price_range.maximum_price.final_price.value;
              } catch (e) {
                max = 0;
              }

              if (discount) {
                return max * ((100 - discount) / 100);
              } else {
                return max;
              }
            })
            .catch((e) => {
              context.logger.error(e);
              return null;
            });
        },
      },
    },
  },
};

Sample query

{
  products(filter: {sku: {in: ["VD03", "VT12"]}}) {
    items {
      name
      sku
      special_price
      price_range {
        maximum_price {
          final_price {
            value
          }
        }
      }
    }
  }
}