Best practices
Learn how to implement resource maintenance best practices when working with commercetools SDKs.
After completing this page, you should be able to:
- Distinguish best practices related to maintaining resources in Composable Commerce.
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 retry handling.
- We will add more extensive and meaningful logging.
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:
- You fetch the Customer.
- You fetch the Customer Group.
- Then you assign the Customer to the Customer Group.
Translating this into our CRUD requests we would have to perform
- A GET request to the Customers endpoint.
- A GET request to the Customer Groups endpoint.
- 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 rootconst 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 purposesconsole.log(JSON.stringify(error, null, 2));// Re-throw the error for handling at a higher levelthrow 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 purposesconsole.log(JSON.stringify(error, null, 2));// Re-throw the error for handling at a higher levelthrow 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 purposesconsole.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 Logger Middleware inside your API client and integrate it with your desired logging solution */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 levelthrow 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 Composable Commerce 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
Lets see how we would handle these errors in the API middleware for both SDKs.
TypeScript SDK
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.
To address HTTP 409 errors, which indicate concurrent modification conflicts, we can employ the withConcurrentModificationMiddleware
. This middleware allows us to define a custom retry strategy.
// Concurrent Modification Middleware Optionsconst concurrentModificationMiddlewareOptions = {concurrentModificationHandlerFn: (version, request) => {console.log(`Concurrent modification error, retry with version ${version}`);request.body.version = version;return JSON.stringify(request.body);},};// Retries the request once with the updated version if a 409 (Conflict) error occurs.
Java SDK
Within the highlighted code, we added the retry policy, where we specified an array of error codes and retries. This will address 503
and 500
errors.
Also included is the addConcurrentModificationMiddleware
, which will automatically retry the request with the correct version number. Using a maxRetries
of 2, delay 200 ms and maxDelay
1000 ms. This will address 409
errors. Learn more about ConcurrentModificationMiddleware
.
ClientService:public static ProjectApiRoot createApiClient() throws IOException {Properties props = new Properties();props.load(ClientService.class.getResourceAsStream("/dev.properties"));String clientId = props.getProperty("CTP_CLIENT_ID");String clientSecret = props.getProperty("CTP_CLIENT_SECRET");String projectKey = props.getProperty("CTP_PROJECT_KEY");projectApiRoot = 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);return projectApiRoot;}
Now you are equipped with the basic operations to maintain your resources in a Composable Commerce Project.