Best practices

Learn how to implement resource maintenance best practices when working with commercetools SDKs.

Ask about this Page
Copy for LLM
View as Markdown

After completing this page, you should be able to:

  • Distinguish best practices related to maintaining resources in commercetools.

On this page we are going to look at four important best practices to develop maintainable, readable, and performant code:

  • We will combine requests without blocking code execution.
  • We will add error handling.
  • We will add extensive and meaningful logging.
  • We will add retry and concurrent modification handling.

Combine requests

Thus far we have used the SDKs to execute single request. Most of the time, we also provided the needed parameters written directly in the code to allow us to concentrate on a single operation. However, this is an anti-pattern for realistic development. Usually you would have your inputs written into variables either from configuration files, databases, or previous requests. Let’s take a look at the latter of these options.

A typical example could be the assignment of a Customer to a Customer Group. Here, you would in a realistic scenario perform three requests:

  1. You fetch the Customer.
  2. You fetch the Customer Group.
  3. Then you assign the Customer to the Customer Group.

Translating this into our CRUD requests we would have to perform

  1. A GET request to the Customers endpoint.
  2. A GET request to the Customer Groups endpoint.
  3. A POST request to the Customers endpoint.

An anti-pattern would be to always block your code execution waiting for every single request to return the result before continuing. We strongly recommend not to do so. Use asynchronicity whenever you can!

Let’s have a look at a potential implementation. As you can see, all hardcoded values are gone.

const customerKey = 'thomas-tools';
const customerGroupId = 'gold';

async function fetchCustomer(customerKey) {
  const customerResponse = await apiRoot
    .customers()
    .withKey({ key: customerKey })
    .get()
    .execute();
  return customerResponse.body;
}

async function updateCustomerGroup(
  customerKey,
  customerGroupId,
  customerVersion
) {
  const updateResponse = await apiRoot
    .customers()
    .withKey({ key: customerKey })
    .post({
      body: {
        version: customerVersion,
        actions: [
          {
            action: 'setCustomerGroup',
            customerGroup: { key: customerGroupId },
          },
        ],
      },
    })
    .execute();
  return updateResponse.body;
}

async function customerUpdateGroupCombine(customerKey, customerGroupId) {
  const customer = await fetchCustomer(customerKey);
  const customerUpdate = await updateCustomerGroup(
    customerKey,
    customerGroupId,
    customer.version
  );
  console.log(
    'Customer group updated successfully:',
    JSON.stringify(customerUpdate, null, 2)
  );
}

customerUpdateGroupCombine(customerKey, customerGroupId);

Error handling

Add error handling to your code to make it robust and to be able to recover from such errors. At a minimum, you might want to learn from the errors to improve your code.

A special note on Java: You might want to use optional classes if you prefer to reduce the code directly used in the request.

assignCustomerToCustomerGroup(customerDraft.key!, "vip-customers")
    .then(log)
    .catch(
      // Handle the error, create objects that the following computation can handle
    );


import { apiRoot } from "../impl/apiClient.js"; // Update to map to your API root


const customerKey = "abcdefghijklm";
const customerGroupId = "silver";
const customerVersion = 1;


async function customerUpdateGroup(customerKey, customerGroupId, version) {
  // Wrap code in a Try/ Catch
 /**
 * Fetches a customer by their key.
 * @param {string} customerKey - The key of the customer to fetch.
 * @returns {Promise<object>} A Promise that resolves with the customer object.
 * @throws {Error} If the customer cannot be fetched.
 */
async function fetchCustomer(customerKey) {
  try {
    const customerResponse = await apiRoot
      .customers()
      .withKey({ key: customerKey })
      .get()
      .execute();
    return customerResponse.body;
  } catch (error) {
    // Log the error for debugging purposes
    console.log(JSON.stringify(error, null, 2));
    // Re-throw the error for handling at a higher level
    throw error;
  }
}

/**
 * Updates a customer's group.
 * @param {string} customerKey - The key of the customer to update.
 * @param {string} customerGroupId - The ID of the customer group to assign.
 * @param {number} customerVersion - The version of the customer being updated.
 * @returns {Promise<object>} A Promise that resolves with the updated customer object.
 * @throws {Error} If the customer group cannot be updated.
 */
async function updateCustomerGroup(customerKey, customerGroupId, customerVersion) {
  try {
    const updateResponse = await apiRoot
      .customers()
      .withKey({ key: customerKey })
      .post({
        body: {
          version: customerVersion,
          actions: [
            {
              action: "setCustomerGroup",
              customerGroup: { key: customerGroupId },
            },
          ],
        },
      })
      .execute();
    return updateResponse.body;
  } catch (error) {
    // Log the error for debugging purposes
    console.log(JSON.stringify(error, null, 2));
    // Re-throw the error for handling at a higher level
    throw error;
  }
}

/**
 * Fetches a customer and updates their customer group.
 * @param {string} customerKey - The key of the customer to update.
 * @param {string} customerGroupId - The ID of the customer group to assign.
 * @throws {Error} If the customer cannot be fetched or the group cannot be updated.
 * This error could occur if the customer key or group ID is invalid,
 * or if there are issues connecting to the commercetools API.
 */
async function customerUpdateGroupCombine(customerKey, customerGroupId) {
  try {
    const customer = await fetchCustomer(customerKey);
    const customerUpdate = await updateCustomerGroup(customerKey, customerGroupId, customer.version);
    console.log("Customer group updated successfully:", JSON.stringify(customerUpdate, null, 2));

  } catch (error) {
    // Log the error for debugging purposes
    console.log(JSON.stringify(error, null, 2));
    // Consider more robust error handling in a production environment,
    // such as logging to an error tracking service or displaying a user-friendly message.
  }
}

customerUpdateGroupCombine(customerKey, customerGroupId);

Logging

Add extensive and very meaningful logging to your code. Remember that writing code never ends! Prepare for future code maintenance and adaptation. This brings us back to the Service class. If you transfer all requests into such service classes you can adapt your strategic and general logging strategy.

/* Configure the LoggerMiddleware inside your API client to log
 * requests and responses. For the full list of configuration options, see:
 * https://docs.commercetools.com/sdk/ts-sdk-middleware#loggermiddleware
 */
const customLoggerMiddleware = {
  logLevel: 'debug',
  httpMethods: ['POST', 'GET'],
  maskSensitiveData: true,
  logger: (method, ...args) => {
    console.log(`[CUSTOM LOGGER] ${method}`, ...args);
  },
};

async function fetchCustomer(customerKey) {
  try {
    const customerResponse = await apiRoot
      .customers()
      .withKey({ key: customerKey })
      .get()
      .execute();
    return customerResponse.body;
  } catch (error) {
    // The error will be logged by the commercetools SDK logging middleware
    // Re-throw the error for handling at a higher level
    throw error;
  }
}
You might want to always log the value x-correlation-id that is present in each response header. This value represents the unique id of the response from the commercetools API. Whenever you need to contact commercetools support and/or need to trace your request, this id is needed. Make sure you have it at hand when needed.

Retry and concurrent modification handling

When sending requests to the commercetools API you may have to handle a situation where you receive an error. commercetools might be shedding load at a certain moment of time, or the request you've sent may be incorrect. Let's look at ways to properly handle common errors like:

  • 500 server unavailability (500 and above).
  • 409 version conflicts

Let's see how we would handle these errors in the API middleware for both SDKs.

TypeScript

Java


With the HttpMiddleware, you can add the retryConfig policy and specify a set of error codes, retries, and delays. This will address 503 and 500 errors. For the full retryConfig options, see the HttpMiddleware reference.

To address HTTP 409 errors, which indicate concurrent modification conflicts, use the concurrent modification middleware. This middleware retries the request with the updated resource version when a 409 Conflict error occurs.

const concurrentModificationMiddlewareOptions = {
  concurrentModificationHandlerFn: (version, request) => {
    request.body.version = version;
    return JSON.stringify(request.body);
  },
};

Use withPolicies to add a retry policy with a specified array of error codes and retries. This will address 503 and 500 errors.

Use addConcurrentModificationMiddleware to automatically retry the request with the correct version number upon 409 errors. Learn more about ConcurrentModificationMiddleware.

ProjectApiRoot apiRoot = ApiRootBuilder.of()
    .defaultClient(
        ClientCredentials.of()
            .withClientId(clientId)
            .withClientSecret(clientSecret)
            .build(),
        ServiceRegion.GCP_AUSTRALIA_SOUTHEAST1.getOAuthTokenUrl(),
        ServiceRegion.GCP_AUSTRALIA_SOUTHEAST1.getApiUrl()
    )
    .addConcurrentModificationMiddleware(2, 200, 1000)
    .withPolicies(policyBuilder ->
        policyBuilder.withRetry(retry ->
            retry
                .maxRetries(3)
                .statusCodes(Arrays.asList(
                    HttpStatusCode.SERVICE_UNAVAILABLE_503,
                    HttpStatusCode.INTERNAL_SERVER_ERROR_500
                ))
        )
    )
    .build(projectKey);

Now you are equipped with the basic operations to maintain your resources in a commercetools Project.

Test your knowledge