Find and manage existing Shopping Lists

Learn how to efficiently query for Shopping Lists and retrieve associated Product details for display.

After completing this page, you should be able to:

  • Formulate efficient queries using relevant predicates (for example, customerId, anonymousId, store) and parameters (for example, limit) to retrieve Shopping Lists via general or in-store API endpoints.
  • Compare methods (Reference Expansion, GraphQL, Product Projections) for fetching enriched product data associated with Shopping Lists, focusing on correct price display and data completeness.

Before creating a new Shopping List, it's crucial to check if a relevant one already exists for the current user. This avoids duplicates and provides a better user experience. You can query for existing lists using specific API endpoints.

Endpoints:
  • General endpoint: /shopping-lists
  • In-Store endpoint: /in-store/key={storeKey}/shopping-lists (Use this when Shopping Lists are associated with specific Stores)

Constructing efficient queries

To query effectively, you need to use optimized query predicates (where clauses) and appropriate query parameters.

Best practices for query predicates

Structure the where clause for optimal performance by prioritizing the most selective fields first. This approach reduces the amount of data the API must scan, leading to faster responses, especially in projects with many Shopping Lists.
  1. customerId or anonymousId: Always include one of these predicates to filter lists belonging to the current user. This is the most critical filter.
  2. store(key = "..."): Include this only if both of the following are true:
    • You are using the general endpoint (/shopping-lists), AND
    • Your Shopping Lists are scoped to a specific store. Omit the store predicate when using the /in-store/... endpoint, as the store context is already defined in the endpoint path.

Key query parameters

  • limit: Set to 1 if you typically expect only one list per user (for example, a single universal wishlist). Increase the value if users can have multiple lists (for example, "Birthday Wishlist", "Grocery List").
  • withTotal: Set to false unless your UI specifically needs to display the total count of matching lists. Disabling this improves performance.
  • sort: Define a sort order if retrieving multiple lists. For example, lastModifiedAt desc to show the most recently updated list first.
  • Pagination: Use cursor-based pagination for large result sets. This improves performance and scalability.

Query predicate examples

  • Find lists for a specific customer ID:
customer(id = "eaf18adc-c61e-4f2c-9d04-b6b8ad51d998")
  • Find lists for a specific customer ID within a specific store (using the general /shopping-lists endpoint):
customer(id = "eaf18adc-c61e-4f2c-9d04-b6b8ad51d998") AND store(key = "dach")
  • Find lists for a specific anonymous session ID:
anonymousId = "9a36d71d-8929-4bee-90cd-787a79471b48"
  • Find lists for a specific anonymous session ID within a specific store (using the general /shopping-lists endpoint):
anonymousId = "9a36d71d-8929-4bee-90cd-787a79471b48" AND store(key = "dach")

TypeScript example: Querying by customer and store (general endpoint)

apiRoot
 .shoppingLists()
 .get({
   /**
    * Query arguments to filter and control the shopping list retrieval.
    * @type {object}
    * @property {number} limit - Max number of results to return.
    * @property {string} sort - How to sort the results (e.g., 'id asc').
    * @property {boolean} withTotal - Whether to include the total count in the response.
    * @property {string} where - The predicate string to filter shopping lists.
    */
   queryArgs: {
     /** Limit the result set to 1 shopping list. */
     limit: 1,
     /** Sort results by ID ascending. */
     sort: 'id asc',
     /** Exclude the total count of matching lists from the response. */
     withTotal: false,
     /**
      * Filter condition using the correct predicate syntax:
      * - Must belong to the customer with the specified ID.
      * - Must be associated with the store having the key "dach".
      */
     where: `customer(id="eaf18adc-c61e-4f2c-9d04-b6b8ad51d998") AND store(key="dach")`,
   },
 })
 .execute()
 .then(
   /**
    * Handles the successful response from the API.
    * Logs the results array directly.
    * @param {ClientResponse<ShoppingListPagedQueryResponse>} response - The API response object.
    * @returns {void}
    */
   (response: ClientResponse<ShoppingListPagedQueryResponse>) => {
     // Log the results array directly from the response body
     console.log(
       'Successfully fetched customer shopping list(s):',
       response.body.results, // Log only the results array
     );
   },
 )
 .catch(
   /**
    * Handles errors that occur during the API request.
    * @param {unknown} error - The error object.
    * @returns {void}
    */
   (error: unknown) => {
     // It's often helpful to log the specific error structure from the SDK
     // if (error.body && error.body.message) {
     //   console.error('Error fetching customer shopping list:', error.body.message, error.body.errors);
     // } else {
     //   console.error('Error fetching customer shopping list:', error);
     // }
      console.error('Error fetching customer shopping list:', error);
   },
 );


TypeScript example: Querying by anonymous ID and store (general endpoint)

apiRoot
.shoppingLists()
.get({
 /**
  * Query arguments to filter and control the shopping list retrieval.
  * @type {object}
  * @property {number} limit - Max number of results to return.
  * @property {string} sort - How to sort the results.
  * @property {boolean} withTotal - Whether to include the total count.
  * @property {string} where - The predicate string to filter shopping lists.
  */
 queryArgs: {
   /** Limit the result set to 1 shopping list. */
   limit: 1,
   /** Optionality sort results by ID ascending. */
   sort: 'id asc',
   /** Exclude the total count of matching lists from the response. */
   withTotal: false,
   /**
    * Filter condition:
    * - Must belong to the anonymous session with the specified ID.
    * - Must be associated with the store having the key "dach".
    */
   where: `anonymousId = "9a36d71d-8929-4bee-90cd-787a79471b48" AND store(key=\"dach\")`,
 },
})
.execute()
.then(
 /**
  * Handles the successful response from the API.
  * Logs the results array directly.
  * @param {ClientResponse<ShoppingListPagedQueryResponse>} response - The API response object.
  * @returns {void}
  */
 (response: ClientResponse<ShoppingListPagedQueryResponse>) => {
   // Log the results array directly from the response body
   console.log(
     'Successfully fetched anonymous shopping list(s):',
     response.body.results, // Log only the results array
   );
 },
)
.catch(
 /**
  * Handles errors that occur during the API request.
  * @param {unknown} error - The error object.
  * @returns {void}
  */
 (error: unknown) => {
   console.error('Error fetching anonymous shopping list:', error);
 },
);





Using the in-store endpoint

If you use the /in-store/key={storeKey}/shopping-lists endpoint, the store context is already defined. You don't need the store(key=...) part in your where clause. The SDK call structure changes slightly:
// Example structure for using the in-store endpoint
apiRoot.inStoreKeyWithStoreKeyValue({ storeKey: "your-store-key" }) // Provide the store key here
  .shoppingLists()
  .get({
    queryArgs: {
      limit: 1,
      withTotal: false,
      // No store predicate needed here
      where: `customer(id="eaf18adc-c61e-4f2c-9d04-b6b8ad51d998")`
    }
  })
  .execute(); // Add .then() and .catch() as needed

Display lists with rich product data

The lineItems within a standard Shopping List response contain only basic Product details, such as productId, variantId, quantity. To display richer Product information in your UI – such as images, current prices, SKUs, or Attributes – you need to fetch additional Product data.
The Challenge:
The basic LineItem object doesn't include fields like price, images, attributes, or sku.
Solutions:

There are three primary approaches to retrieve this richer data: using reference expansion, GraphQL, and Product Projections.

Approach 1: Reference expansion

  • How it Works: Use the expand query parameter when fetching the Shopping List. You specify which references within the list you want the API to "expand" or include inline in the response. For Product data, you'd typically expand lineItems[*].variant.
  • Pros:
    • Simple to implement – requires only adding the expand parameter to your existing list query.
  • Cons:
    • No Product Price Selection: Prices returned on the expanded variant are base prices. This method does not automatically apply the correct price based on customer group, currency, or country. You would need to implement complex client-side logic to determine the correct display price.
    • Limited caching: Doesn't integrate easily with application-side Product caching strategies, as Product data is tied to the list fetch.

TypeScript example: Reference expansion (HTTP API)

// Example GET request for a shopping list using the 'expand' parameter for variant data.apiRoot
  .shoppingLists()
  .get({
    /**
     * Query arguments to filter and control the shopping list retrieval.
     * @type {object}
     * @property {number} limit - Max number of results to return.
     * @property {string} sort - How to sort the results (e.g., 'id asc').
     * @property {boolean} withTotal - Whether to include the total count in the response.
     * @property {string} where - The predicate string to filter shopping lists.
     */
    queryArgs: {
      /** Limit the result set to 1 shopping list. */
      limit: 1,
      /** Sort results by ID ascending. */
      sort: 'id asc',
      /** Exclude the total count of matching lists from the response. */
      withTotal: false,
      where: `id = "eaf18adc-r61e-4f2c-9d04-b6b8ad51d998"`,
     expand: ['lineItems[*].variant']  // Expand variants within line items

     },
  })
  .execute()
  .then()
  .catch();

Approach 2: GraphQL API

  • How it works: Construct a single GraphQL query that retrieves the Shopping List and precisely specifies the Product fields you need - such as name, image, price, availability attributes - for each lineItem variant. The API returns both the list data and the requested Product details nested in a single response. You can also include price selection parameters directly in the query.
  • Pros:
    • Single API Request: Fetches both the list and the required Product details efficiently in one call.
    • Precise Data Fetching: Avoids over-fetching by allowing you to request only the specific fields needed for your UI.
    • Supports Product Price Selection: Price selection parameters can be applied directly within the GraphQL query.
    • Caching Friendly: Integrates well with external caching strategies for non-sensitive data returned in the response (for example, name, images, attributes).
  • Cons:
    • Requires familiarity with GraphQL query syntax.
    • May require client-side logic to parse the nested response and potentially map it to UI-specific data structures, although often less complex mapping than Approach 2.

GraphQL example: Reference Expansion Query

// Example GraphQL query fetching a shopping list and expanding variant images.query getShoppingListWithProductImages($shoppingListId: String!) {
 shoppingList(id: $shoppingListId) {
   id
   key
   name(locale: "en")
   lineItems {
     id
     name(locale: "en")
     productId
     quantity
     variant {
       id
       sku
       images {
         url
         label
       }
     }
     productSlug(locale: "en")
   }
   textLineItems {
     name(locale: "en")
     id
     quantity
     description(locale: "en")
   }
   slug(locale: "en")
 }
}

TypeScript example: Executing GraphQL expansion query

/**
* The GraphQL query string to fetch a shopping list with line item variant images.
* It expects a variable $shoppingListId of type String!.
* @type {string}
*/
const graphqlQuery = `
  query getShoppingListWithProductImages($shoppingListId: String!) {
shoppingList(id: $shoppingListId) {
  id
  key
  name(locale: "en")
  lineItems {
    id
    name(locale: "en")
    productId
    quantity
    variant {
      id
      sku
      images {
        url
        label
      }
    }
    productSlug(locale: "en")
  }
  textLineItems {
    name(locale: "en")
    id
    quantity
    description(locale: "en")
  }
  slug(locale: "en")
}
}


`;


/**
* Define the variables required by the GraphQL query.
* The keys here must match the variable names defined in the query string (without the $).
* @type {object}
*/
const variables = {
shoppingListId: '0a6eaa69-c255-4439-9394-1ea8254a61f0', // <-- ** IMPORTANT: Set the actual Shopping List ID here **
};


// Execute the GraphQL request using apiRoot
apiRoot
.graphql()
.post({
  /**
   * The body of the GraphQL POST request.
   * @type {object}
   * @property {string} query - The GraphQL query document.
   * @property {object} [variables] - An object containing values for variables defined in the query.
   * @property {string} [operationName] - Specifies which operation to run if the query string contains multiple operations.
   */
  body: {
    query: graphqlQuery,
    variables: variables,
    operationName: 'getShoppingListWithProductImages', // Optional, but good practice if query is named
  },
  // headers?: { [key: string]: string | string[] } // Optional: Add custom headers if needed
})
.execute()
// If using stronger types: .execute<ClientResponse<ShoppingListGraphQLResponse>>()
.then((response) => {
  // Successful HTTP request (status 2xx).
  // The actual GraphQL result is in response.body.
  // Note: GraphQL can return errors even with a 200 OK status. Check response.body.errors.

  console.log('GraphQL request executed.');

  if (response.body.errors) {
    // Log any errors returned by the GraphQL endpoint itself
    console.warn('GraphQL query returned errors:');
    console.warn(JSON.stringify(response.body.errors, null, 2));
  }


  if (response.body.data) {
    // Log the data payload if present
    console.log('GraphQL response data:');
    console.log(JSON.stringify(response.body.data, null, 2)); // Pretty print the data
    // Example access: const listName = response.body.data?.shoppingList?.name;
  } else {
    console.log('No data returned in GraphQL response.');
  }
})
.catch((error) => {
  // Handles HTTP-level errors (e.g., network issues, 4xx/5xx status codes)
  // or errors during SDK processing.
  console.error('Error executing GraphQL request:');
  // The 'error' object might contain details like status code, headers, and body
  console.error(JSON.stringify(error, null, 2));
});


GraphQL example: Response snippet (expansion)

// Example snippet of a GraphQL response showing expanded variant data.{
  "data": {
    "shoppingList": {
      "id": "587f9697-07b4-429c-88d8-93eba41807a1",
      "key": "my-shopping-list",
      "name": "My shopping list",
      "lineItems": [
        {
          "id": "316e06a6-6da2-4c72-9045-a197a1609f47",
          "name": "Clutch ”Carol” Liebeskind black",
          "productId": "c61b8923-487c-4900-9a3c-e94e9737df77",
          "quantity": 1,
          "variant": {
            "id": 1,
            "sku": "A0E2000000024AX",
            "images": [
              {
                "url": "https://s3-eu-west-1.amazonaws.com/commercetools-maximilian/products/080721_1_medium.jpg",
                "label": null
              }
            ]
          },
          "productSlug": "liebeskind-clutch-carol-black"
        }
      ],
      "textLineItems": [
        {
          "name": "My shopping list item",
          "id": "0f92302b-e483-4682-8746-9e0b4cb47b95",
          "quantity": 5,
          "description": "This is a good gift idea"
        }
      ],
      "slug": "my-shopping-list"
    }
  }
}

TypeScript example: Fetching Product Projections

// Example showing the multi-step process: fetch list, extract IDs, fetch product projections.
// --- Configuration ---
const shoppingListIdToFetch = '0a6eaa69-c255-4439-9394-1ea8254a61f0'; // <-- Replace with the actual Shopping List ID

apiRoot
 .shoppingLists()
 .get({
   /**
    * Query arguments to fetch a specific shopping list by its ID.
    */
   queryArgs: {
     /** Limit to 1, as ID is unique. */
     limit: 1,
     /** Sorting is less relevant when fetching by unique ID. */
     // sort: 'id asc',
     /** Exclude total count. */
     withTotal: false,
     /** Filter by the specific Shopping List ID. */
     where: `id = "${shoppingListIdToFetch}"`,
   },
 })
 .execute()
 .then(
   /**
    * Handles the successful response for the shopping list fetch.
    * Then proceeds to fetch product projections for its line items.
    * @param {ClientResponse<ShoppingListPagedQueryResponse>} shoppingListResponse - The API response for the shopping list.
    * @returns {Promise<void>}
    */
   async (
     shoppingListResponse: ClientResponse<ShoppingListPagedQueryResponse>,
   ): Promise<void> => {
     const shoppingList = shoppingListResponse.body.results?.[0];

     if (!shoppingList) {
       console.warn(
         `Shopping list with ID ${shoppingListIdToFetch} not found.`,
       );
       return; // Stop processing if list not found
     }

     console.log(
       `Successfully fetched shopping list: ${shoppingList.id} (Name: ${
         shoppingList.name['en'] ?? 'N/A' // Adjust locale as needed
       })`,
     );

     // Check if there are any product line items
     const productLineItems = shoppingList.lineItems?.filter(
       (item): item is ShoppingListLineItem & { productId: string } =>
         !!item.productId, // Filter out text line items and ensure productId exists
     );

     if (!productLineItems || productLineItems.length === 0) {
       console.log('Shopping list contains no product line items.');
       return; // Stop if no product items
     }

     // --- Fetch Product Projections for Line Items ---

     // 1. Extract unique Product IDs from the line items
     const productIds = Array.from(
       new Set(productLineItems.map((item) => item.productId)),
     );

     console.log(
       `Found ${productIds.length} unique product ID(s) in line items:`,
       productIds,
     );
     console.log('Fetching corresponding product projections...');

     try {
       // 2. Construct the 'where' clause for fetching multiple products by ID
       const productIdsFormatted = productIds.map((id) => `"${id}"`).join(',');
       const productWhereClause = `id in (${productIdsFormatted})`;

       // 3. Make a single request to fetch all relevant product projections
       const productProjectionResponse: ClientResponse<ProductProjectionPagedQueryResponse> =
         await apiRoot
           .productProjections()
           .get({
             queryArgs: {
               /** Fetch only published product data. */
               staged: false,
               /** Filter by the list of product IDs found. */
               where: productWhereClause,
               /** Ensure limit is high enough if many products, or implement pagination */
               limit: productIds.length, // Fetch up to the number of unique IDs
             },
           })
           .execute();

       const fetchedProjections: ProductProjection[] =
         productProjectionResponse.body.results;

       console.log(
         `Successfully fetched ${fetchedProjections.length} product projection(s):`,
       );

       // 4. Log or process the fetched projections
       fetchedProjections.forEach((projection) => {
         console.log(
           `  - Product ID: ${projection.id}, Name: ${
             projection.name['en'] ?? 'N/A' // Adjust locale
           }`,
         );
         // You can now use the 'fetchedProjections' array which contains
         // the details for the products in the shopping list.
         // Example: Match back to line items if needed, display details, etc.
       });

       if (fetchedProjections.length < productIds.length) {
         console.warn(
           'Warning: Not all product IDs from the shopping list resulted in a fetched projection. Some products might be unpublished or deleted.',
         );
       }
     } catch (productError: unknown) {
       console.error(
         'Error fetching product projections for shopping list items:',
         productError,
       );
       // Handle errors during the product fetch phase
     }
   },
 )
 .catch(
   /**
    * Handles errors during the initial shopping list fetch.
    * @param {unknown} error - The error object.
    * @returns {void}
    */
   (error: unknown) => {
     console.error(
       `Error fetching shopping list with ID ${shoppingListIdToFetch}:`,
       error,
     );
     // Handle errors during the initial shopping list fetch
   },
 );

Choosing the right approach

  • Reference Expansion (REST API): Use for maximum simplicity if accurate, context-aware pricing and optimized data fetching are not critical requirements.
  • GraphQL API: Use when you want to combine the benefits of Product Price Selection and precise data fetching within a single API request. It's often the most efficient approach, especially for larger lists, provided you are comfortable working with GraphQL. It also integrates well with caching strategies.
  • Fetching Product Projections (REST API): Use when accurate Product Price Selection is essential, when you need fine-grained control over data loading (for example, progressive loading), or if you prefer REST APIs and need to integrate with application-side caching. Be prepared for multiple API calls and client-side data mapping.

Test your knowledge