13 minutes
h1

Without a gateway layer, the storefront client makes four separate API calls, each with its own authentication, error handling, and network round-trip. API Mesh for Adobe Developer App Builder solves this by combining all four sources into a single GraphQL endpoint that the storefront queries once.

A headless Adobe Commerce storefront typically needs data from more than one source to render a single page. The product detail page might need product data from Commerce GraphQL, real-time inventory from an ERP (Enterprise Resource Planning), delivery estimate from a logistics API, and pricing from a customer-specific price engine.

This guide explains how to design, build, and optimize that unified graph for a Commerce headless implementation. It covers the three handler types that define how sources are connected, how custom resolvers extend the schema beyond what any single source provides, how authentication is handled per source without exposing credentials to the storefront client, and how caching is configured to keep the unified endpoint fast.

What API Mesh is and what it is not

API Mesh for Adobe Developer App Builder is a managed, cloud-hosted GraphQL gateway. It is not an App Builder application you deploy and manage; it is a service you configure via a JSON file (mesh.json) and deploy using the aio CLI. Adobe manages the hosted runtime and operational infrastructure for the mesh service, including WAF (web application firewall) and DDoS protection. Your responsibility is the mesh configuration: which sources to connect, how to authenticate each one, which fields to expose or transform, and how to cache responses.

The output of deploying a mesh is a single Edge Mesh Endpoint URL; a GraphQL endpoint that accepts queries. When a query is sent to this endpoint, API Mesh fans out the sub-queries to the configured source systems, receives their responses, merges the results according to the schema, and returns a single unified response. In a typical mesh architecture, the storefront primarily interacts with this unified mesh endpoint; some implementations do combine Mesh with direct API calls or bypass Mesh selectively for performance-sensitive paths, so treat this as the strongly recommended default pattern rather than an absolute rule.

What API Mesh is not: it is not a replacement for App Builder actions for complex business logic. It is not a data transformation service for bulk operations. It is not an event-driven integration layer, that is what Adobe I/O Events and the Integration Starter Kit provide. API Mesh is specifically for synchronous, query-time data federation combining data from multiple sources into a single GraphQL response for a storefront query.

NOTE
API Mesh is available as part of Adobe Developer App Builder, which requires an active Adobe Commerce license (PaaS, ACCS, or ACO) or an Adobe Experience Cloud license. The mesh endpoint is provisioned per App Builder workspace (Development, Staging, Production). Each workspace has its own mesh configuration and endpoint URL, use separate workspaces per environment to avoid Production configuration changes affecting Development testing.

The three handler types: how sources are connected

A mesh source is defined in the sources array in mesh.json. Each source has a name, a handler type, and the handler-specific configuration. API Mesh supports three handler types, and this article covers how to connect and combine them into one graph; it does not cover what happens when two sources define overlapping or conflicting type names, or how namespace collisions are resolved during schema stitching, a real consideration once a mesh grows beyond two or three sources. Plan naming conventions across sources deliberately rather than discovering a collision after the fact.

GraphQL handler

Use the graphql handler for any source that exposes a GraphQL endpoint. This is the handler for Adobe Commerce's native GraphQL API and for Catalog Service. The handler introspects the source schema during mesh configuration and update operations and includes its types and queries in the unified graph; this basic schema ingestion is largely automatic, though normalization, naming conflicts, resolver extensions, transformations, and schema pruning often require additional work beyond the initial automatic ingestion.

{
  "meshConfig": {
    "sources": [
      {
        "name": "Commerce",
        "handler": {
          "graphql": {
            "endpoint": "https://your-store.com/graphql",
            "operationHeaders": {
              "Store": "{context.headers['store']}",
              "Content-Currency": "{context.headers['content-currency']}"
            }
          }
        }
      }
    ]
  }
}

The operationHeaders block forward headers from the incoming storefront request to the source. In the example above, the Store and Content-Currency headers are extracted from the request context and passed to the Commerce GraphQL endpoint. This is how per-store-view and per-currency requests are correctly routed without hard-coding values in the mesh configuration.

OpenAPI handler

Use the openapi handler for REST APIs that expose an OpenAPI (formerly Swagger) specification. The handler reads the OpenAPI spec, generates GraphQL types from the REST resource definitions, and exposes them as queries and mutations in the unified graph. This is the handler for Commerce's REST API, for third-party OMS (Order Management System) REST APIs, and for any external service that provides an OpenAPI spec.

{
  "name": "CommerceREST",
  "handler": {
    "openapi": {
      "source": "https://your-store.com/rest/all/schema?services=all",
      "operationHeaders": {
        "Authorization": "Bearer {context.headers['x-commerce-token']}",
        "Content-Type": "application/json"
      }
    }
  }
}

The Commerce REST API schema at /rest/all/schema?services=all covers every REST endpoint in the Commerce API surface. When using the OpenAPI handler for Commerce REST, filter the schema to include only the endpoints you need using the includeByIdentity configuration. Introspecting the full Commerce REST schema adds unnecessary types to the unified graph, slows schema generation, and creates confusion for developers querying the mesh.

JsonSchema handler

Use the JsonSchema handler for REST endpoints that do not have an OpenAPI spec, a custom internal API, a third-party service with only a JSON response sample, or a legacy endpoint that predates OpenAPI. You define the request and response structure using JSON Schema files, and the handler generates the corresponding GraphQL types. The capitalisation shown here, graphql, openapi (lowercase), and JsonSchema (capitalised), matches Adobe's current handler documentation and code samples.

{
  "name": "LogisticsAPI",
  "handler": {
    "JsonSchema": {
      "baseUrl": "https://logistics.partner.com/api",
      "operationHeaders": {
        "X-API-Key": "{env.LOGISTICS_API_KEY}"
      },
      "operations": [
        {
          "type": "Query",
          "field": "deliveryEstimate",
          "path": "/estimate/{args.sku}/{args.postcode}",
          "method": "GET",
          "responseSchema": "./schemas/delivery-estimate-response.json"
        }
      ]
    }
  }
}
NOTE
The "{env.LOGISTICS_API_KEY}" reference in operationHeaders. API Mesh supports environment variable references in the mesh configuration. Sensitive credentials; API keys, bearer tokens, client secrets should never be hard-coded in the mesh.json file. Store them as environment variables in the App Builder workspace and reference them via {env.VARIABLE_NAME}. The mesh configuration is committed to source control; the environment variables are not.
IMPORTANT
The JsonSchema handler requires you to maintain the JSON Schema files alongside the mesh configuration. If the source API changes its response format and you do not update the schema files, schema drift can lead to missing or improperly mapped fields, the mesh may not surface an obvious error depending on resolver behavior and the source's response structure. Add API response validation to your CI pipeline to catch schema drift before it reaches production.

A complete mesh: Commerce, Catalog Service, and OMS

A typical headless Commerce PDP requires data from three sources: Commerce GraphQL for product attributes and pricing, Catalog Service for enriched catalog data and variant selection, and a third-party OMS for real-time inventory by fulfillment location. Here is how all three are combined in a single mesh configuration:

{
  "meshConfig": {
    "sources": [
      {
        "name": "Commerce",
        "handler": {
          "graphql": {
            "endpoint": "https://your-store.com/graphql",
            "operationHeaders": {
              "Store": "{context.headers['store']}",
              "Authorization": "{context.headers['authorization']}"
            }
          }
        }
      },
      {
        "name": "CatalogService",
        "handler": { "graphql": {
          "endpoint": "https://catalog-service-endpoint",
          "operationHeaders": {
            "x-api-key": "{env.CATALOG_SERVICE_API_KEY}",
            "Magento-Store-View-Code": "{context.headers['store']}"
          }
        } }
      },
      {
        "name": "OMS",
        "handler": {
          "JsonSchema": {
            "baseUrl": "https://oms.partner.com/api/v2",
            "operationHeaders": { "X-API-Key": "{env.OMS_API_KEY}" },
            "operations": [{
              "type": "Query",
              "field": "inventoryBySku",
              "path": "/inventory/{args.sku}",
              "method": "GET",
              "responseSchema": "./schemas/oms-inventory.json"
            }]
          }
        }
      }
    ]
  }
}

Once deployed, the storefront client sends a single query to the mesh endpoint that retrieves product data from Commerce, catalog enrichment from Catalog Service, and inventory from the OMS in one round-trip. The mesh fans out the sub-queries in parallel where sources are independent, waits for all responses, and returns the merged result.

TIP
Independent source calls can often be executed in parallel by API Mesh, which is one of the primary performance benefits of using a mesh, though resolver dependencies, execution plans, batching behaviour, and source orchestration can all affect how consistently parallelism is achieved. A storefront making three separate API calls to Commerce, Catalog Service, and OMS sequentially pays the latency of all three combined. A mesh executing them in parallel is typically bounded by the slowest source rather than the sum of all three, for a PDP where each call takes 200 to 300ms, that generally means a mesh response closer to 300ms than 900ms, though real-world results depend on network conditions, edge overhead, and whether any resolver chaining is involved. The directional point, parallel fan-out beats sequential calls, holds regardless of the exact numbers.

Custom resolvers: extending the schema beyond source fields

The three handler types give you the fields that each source already exposes. Custom resolvers; the additionalResolvers configuration in mesh.json, lets you add fields that combine data from multiple sources, apply business logic, or transform the data before it reaches the storefront client.

A resolver is a JavaScript file uploaded alongside the mesh configuration. It defines how a specific field in the unified schema is resolved, what data to fetch, from which source, and how to transform it. The most common use case: adding a computed field that combines data from two sources into one response field that neither source provides on its own.

An example: adding a savingsAmount field to a product that computes the difference between the regular price and the customer-specific price; data from Commerce GraphQL formatted as a currency string:

// additional-resolvers.js

module.exports = {

resolvers: {

// Extend the ProductInterface type with a computed field

ProductInterface: {

savingsAmount: {

// This field depends on the price_range already fetched

// from the Commerce source - no extra network call needed

resolve: (product) => {

const regular = product.price_range?.maximum_price?.regular_price?.value;

const final = product.price_range?.maximum_price?.final_price?.value;

if (!regular || !final || regular === final) return null;

const savings = (regular - final).toFixed(2);

const currency = product.price_range.maximum_price.final_price.currency;

return `${currency} ${savings}`;

}

}

}

}

};

Reference the resolver file in the mesh configuration:

{
  "meshConfig": {
    "sources": [ /* ... */ ],
    "additionalResolvers": [
      "./additional-resolvers.js"
    ]
  }
}

The savingsAmount field is now available on every product query through the mesh; the storefront can include it in any product query without any additional API call, because the resolver computes it from data that is already fetched as part of the product query.

NOTE
Custom resolvers that fetch additional data from a source for each item in a list create an N+1 query problem: one resolver call per list item, each making a separate API call to the source. For example, a resolver that fetches delivery estimates per SKU on a PLP (Product Listing Page) with 48 products generates 48 sequential calls. Mitigate this by using batching in the resolver, collect all SKUs, make one batched API call, and map results back to individual items. The API Mesh samples repository includes a batching example.

Authentication passthrough: keeping credentials off the client

One of the most operationally important benefits of API Mesh is keeping source API credentials off the storefront client. Without a gateway, a headless storefront that calls a third-party OMS or pricing API directly must either expose the API key in the client-side JavaScript (a security risk) or proxy requests through its own backend (an architectural complication). The mesh gateway handles all backend authentication using credentials stored as environment variables, the storefront client authenticates only with the mesh endpoint.

The authentication pattern depends on the credential type:

The storefront client sees only the mesh endpoint URL. It has no visibility into which backends the mesh calls, which credentials those backends require, or how the responses are merged. Rotating a backend API key requires updating the App Builder environment variable, not a storefront code change or deployment.

Caching: making the unified endpoint fast

API Mesh caching is disabled by default, and must be explicitly opted in via meshConfig.responseConfig.cache: true. The rationale: once caching is configured, API Mesh operates as a public cache driven by cache-control headers from sources, and caching inappropriate content (authenticated user data, session-specific responses) would create data leakage between shoppers. API Mesh will never cache responses containing the private or no-store directives, but your sources are still responsible for returning appropriate cache-control headers, Mesh itself is unaware of your compliance requirements. Before enabling caching, understand what each source returns in its Cache-Control header.

There are two caching options: native caching (API Mesh's built-in cache, configured via meshConfig.responseConfig.cache: true) and third-party CDN caching (bringing your own Fastly, Cloudflare, or other CDN in front of the mesh endpoint). For Commerce PaaS deployments that already have Fastly provisioned, the third-party Fastly caching option lets you reuse the existing CDN contract and tooling.

To enable native caching with a specific TTL for a source:

{
  "name": "CatalogService",
  "handler": { /* ... */ },
  "responseConfig": {
    "cache": {
      "cacheControl": "public, max-age=300"
    }
  }
}

When multiple sources are involved in a single query and their cache-control values conflict, API Mesh selects the lowest and most restrictive value. The full resolution logic: the no-store directive supersedes all others outright; for numeric directives (max-age, min-fresh, max-stale, s-maxage, stale-if-error, stale-while-revalidate), the lowest value wins; for boolean-style directives (public, private, immutable, no-cache, no-transform, must-revalidate, proxy-revalidate, must-understand), any source that includes the directive causes it to be added to the merged response. In practice, if Commerce returns max-age=3600 and the OMS inventory endpoint returns max-age=60, the merged response is cached for 60 seconds, the shorter of the two. Design your source cache-control headers thoughtfully: high-frequency changing data (inventory) should return short TTLs; stable data (product attributes, category trees) should return long TTLs.

For Commerce PaaS deployments using Fastly with API Mesh, add the x-commerce-bypass-fastly-cache: true header in the mesh's operationHeaders for the Commerce source. This is a documented Fastly integration pattern (Dynamic cache control with Fastly). Adobe's documentation adds one more required step beyond the header itself: you must also specify which headers to preserve in responseConfig.headers, typically x-magento-cache-id, x-magento-tags, set-cookie, pragma, cache-control, expires, x-content-type-options, x-xss-protection, and x-platform-server. This prevents Fastly from caching the sub-requests that API Mesh makes to Commerce; those responses should be cached at the mesh level, not again at the Fastly level, to avoid cache key conflicts.

Deploying and managing a mesh

The full CLI lifecycle for deploying and managing a mesh:

TIP

Keep a separate mesh.json per App Builder workspace (Development, Staging, Production) managed in a monorepo alongside the storefront code. Use environment-specific source endpoints in each workspace configuration. The mesh ID is workspace-specific, the storefront's environment variables for the GraphQL endpoint URL should reference the correct workspace endpoint per deployment environment.

This section covers the deploy/update/status lifecycle but not observability once a mesh is live in production, tracing individual resolver calls, logging source response times, and monitoring overall mesh performance are worth planning for separately, since debugging a slow or failing mesh in production benefits from the same visibility you'd want into any other API gateway.

Performance considerations

API Mesh introduces a network hop between the storefront and the source APIs. For most Commerce headless implementations, the performance trade-off is strongly positive; the parallel source fan-out and edge caching outweigh the added hop latency. But several patterns can negate these benefits, and one consideration this section doesn't otherwise cover is protecting upstream sources from the mesh itself: resolver-level throttling, concurrency control, and general abuse mitigation matter once a mesh aggregates traffic from a storefront in front of multiple backend APIs, some of which may have their own rate limits.

Key takeaways

Adobe tools and resources for API Mesh and Commerce integration

Actionable next steps