Implementing an API Extension

API Extensions allow you to modify the response of an API call, either by validating the request or by running additional updates.

In this tutorial, we'll write two small API Extensions: The first one will validate that not more than ten items can be added to a cart. The second one will add a mandatory insurance if any item in a cart costs $500 or more. Finally, we'll show how conditional triggers can help eliminate unnecessary API calls to your service. Using the mandatory insurance example, we will configure our Extension to only be called when a cart has items with a value of $500 or more.

The examples include downloads for AWS Lambda, Azure Functions, and Google Cloud Functions, for you to be able to start experimenting with API Extensions. However, API Extensions can be run in any way you like as long as they're accessible via HTTP.

Setting up an API Extension

An API Extension gets called during the execution of API calls that the Extension is registered for. Let's register an API Extension that will be called whenever a Cart is created, assuming it's deployed at https://example.org/extension:

{
"destination": {
"type": "HTTP",
"url": "https://example.org/extension"
},
"triggers": [
{
"resourceTypeId": "cart",
"actions": ["Create"]
}
]
}

As you can see, we've specified a single trigger on the Cart resource, with a single action.

Integrations with some Function-as-a-Service providers also exist. In the following example, we're specifying a function deployed on Azure that is called whenever a Cart is created or updated:

{
"destination": {
"type": "HTTP",
"url": "https://example.azurewebsites.net/api/extension",
"authentication": {
"type": "AzureFunctions",
"key": "VQNhktml0D1OehPlsja1qEjzJFYD01jGmjua5zmRR7W3kaGMGRAaSA=="
}
},
"triggers": [
{
"resourceTypeId": "cart",
"actions": ["Create", "Update"]
}
],
"key": "my-extension"
}

Here we're adding an API Extension that runs on AWS Lambda.

{
"destination": {
"type": "AWSLambda",
"arn": "arn:aws:lambda:eu-west-1:123456789:function:extension",
"accessKey": "ABCDEFQAVO2P5FYMZKMA",
"accessSecret": "abcdeffMVqh1mL0Pcs7OYaPJj3plpttaGR+htcgR"
},
"triggers": [
{
"resourceTypeId": "cart",
"actions": ["Create", "Update"]
}
]
}

And here we're adding an API Extension that runs on Cloud Functions and is authorized through Identity and Access Management (IAM). Further details on the authorization process can be found in the API documentation.

{
"destination": {
"type": "GoogleCloudFunction",
"url": "https://europe-west3-project.cloudfunctions.net/your-function"
},
"triggers": [
{
"resourceTypeId": "cart",
"actions": ["Create", "Update"]
}
]
}

The following steps will assume that you've set up an API Extension already. Examples are provided for AWS Lambda, Azure Functions, and Google Cloud Functions, but it should be easy to port them to other frameworks. The code samples shown work with Google Cloud Functions.

Receiving the API request

We'll receive an Input that contains the resource that triggered the Extension. If the API Extension approves the resource (and doesn't request any changes), it will be persisted as-is. Let's try it out!

exports.printBody = (req, res) => {
// Log the request we receive
console.log(req.body);
// Return a 200 status code with an empty body to approve the API request
res.status(200).end();
};

Let's create a Cart:

$curl -sH "Authorization: Bearer {access_token}" -X POST -d '{"currency": "USD"}' https://api.{region}.commercetools.com/{projectKey}/carts

The API will respond with a Cart like this:

{
"id": "449cf0cd-cd9f-4c7b-9efa-8bbe0185c899",
"version": 1,
"createdAt": "2018-01-17T14:24:37.584Z",
"lastModifiedAt": "2018-01-17T14:24:37.584Z",
"lineItems": [],
"cartState": "Active",
"totalPrice": {
"currencyCode": "USD",
"centAmount": 0
},
"taxedPrice": {
"totalNet": {
"currencyCode": "USD",
"centAmount": 0
},
"totalGross": {
"currencyCode": "USD",
"centAmount": 0
},
"taxPortions": []
},
"customLineItems": [],
"discountCodes": [],
"inventoryMode": "None",
"taxMode": "Platform",
"taxRoundingMode": "HalfEven",
"taxCalculationMode": "LineItemLevel",
"refusedGifts": [],
"origin": "Customer"
}

Let's check if the API Extension was actually called! In the logs of the API Extension, we should now see an entry like this:

2018-01-17T14:21:10.804 Function started (Id=0a1fc6ba-0626-4368-91cd-6c6cafb712e5)
2018-01-17T14:21:10.882 { action: 'Create',
resource:
{ typeId: 'cart',
id: '449cf0cd-cd9f-4c7b-9efa-8bbe0185c899',
obj:
{ id: '449cf0cd-cd9f-4c7b-9efa-8bbe0185c899',
version: 1,
createdAt: 2018-01-17T14:24:37.584Z,
lastModifiedAt: 2018-01-17T14:24:37.584Z,
lineItems: [],
cartState: 'Active',
totalPrice: [Object],
taxedPrice: [Object],
customLineItems: [],
discountCodes: [],
inventoryMode: 'None',
taxMode: 'Platform',
taxRoundingMode: 'HalfEven',
taxCalculationMode: 'LineItemLevel',
refusedGifts: [],
origin: 'Customer' } } }
2018-01-17T14:21:10.913 Function completed (Success, Id=0a1fc6ba-0626-4368-91cd-6c6cafb712e5, Duration=11ms)

As we can see, the API Extension got called, and it did receive the Cart object. With the exception of the timestamps (which are generated after the API Extension has given its approval), the objects are identical.

Validate the maximum number of Line Items in the Cart

Let's try to do something useful with the API Extension! In this first example, we'll validate that Carts can not reach a state that we won't allow to order. For our use case, every Cart containing more than ten Line Items would be invalid.

First, we'll have to check the Line Items in the updated Cart that has been given as input to the API Extension. If we find more than ten Line Items in this Cart, we're responding with a 400 Bad Request HTTP status code and supply an error message. We'll also include a known error code - in this case, InvalidInput.

exports.validateMaximumOfTenItems = (req, res) => {
var cart = req.body.resource.obj;
var itemsTotal = cart.lineItems.reduce((acc, curr) => {
return acc + curr.quantity;
}, 0);
if (itemsTotal <= 10) {
res.status(200).end();
} else {
res.status(400).json({
errors: [
{
code: 'InvalidInput',
message: 'You can not put more than 10 items into the cart.',
},
],
});
}
};

You can download this example for AWS Lambda, Azure Functions and Google Cloud Functions.

Let's create a Cart with 15 Line Items that should fail the validation because it is exceeding the limit of 10 Line Items:

{
"currency": "USD",
"lineItems": [
{
"productId": "f018cbd1-8fd3-44b0-9071-4e60d3d66ad9",
"quantity": 15
}
]
}

As expected the API responds with the error forwarded from the API Extension!

{
"statusCode": 400,
"message": "You can not put more than 10 items in the cart.",
"errors": [
{
"code": "InvalidInput",
"message": "You can not put more than 10 items in the cart.",
"errorByExtension": {
"id": "23ab3fcd-ec6a-41a5-bcc6-86add42cccc3",
"key": "my-extension"
}
}
]
}

Some extra fields are added, including the object errorByExtension indicating that the error occurred on the API Extension itself. This object contains identifiers for the API Extension that produced the error message helping us to analyze the error.

Add an Custom Line Item to the Cart

In the second example, we're looking for a high value item in the Cart. In this example, an item is considered high-value when its price is $500 or more. If at least one such item exists in the Cart, we're adding mandatory insurance for it. We'll also have to remove the insurance if the valuable item was removed from the Cart.

To add the mandatory insurance to the Cart, we're sending an AddCustomLineItem update action (which you may recognize from the regular /carts endpoints). To remove the insurance, we'll send the RemoveCustomLineItem update action.

exports.addMandatoryInsurance = (req, res) => {
// Use an ID from your project!
var taxCategoryId = 'af6532f2-2f74-4e0d-867f-cc9f6d0b7c5a';
var cart = req.body.resource.obj;
// If the cart contains any line item that is worth more than $500,
// mandatory insurance needs to be added.
var itemRequiresInsurance = cart.lineItems.find((lineItem) => {
return lineItem.totalPrice.centAmount > 50000;
});
var insuranceItem = cart.customLineItems.find((customLineItem) => {
return customLineItem.slug == 'mandatory-insurance';
});
var cartRequiresInsurance = itemRequiresInsurance != undefined;
var cartHasInsurance = insuranceItem != undefined;
if (cartRequiresInsurance && !cartHasInsurance) {
res.status(200).json({
actions: [
{
action: 'addCustomLineItem',
name: { en: 'Mandatory Insurance for Items above $500' },
money: {
currencyCode: cart.totalPrice.currencyCode,
centAmount: 1000,
},
slug: 'mandatory-insurance',
taxCategory: {
typeId: 'tax-category',
id: taxCategoryId,
},
},
],
});
} else if (!cartRequiresInsurance && cartHasInsurance) {
res.status(200).json({
actions: [
{
action: 'removeCustomLineItem',
customLineItemId: insuranceItem.id,
},
],
});
} else {
res.status(200).end();
}
};

You can download this example for AWS Lambda, Azure Functions and Google Cloud Functions.

Let's try it out! Here, we're creating a Cart with a Line Item that is worth more than $500:

{
"currency": "USD",
"lineItems": [
{
"productId": "f018cbd1-8fd3-44b0-9071-4e60d3d66ad9"
}
]
}

In the API response, we'll find that the insurance has been added to the Custom Line Items:

{
"lineItems": [{
"id": "85f926ba-5bdb-425f-9bf7-d31130dc9502",
"productId": "f018cbd1-8fd3-44b0-9071-4e60d3d66ad9",
"totalPrice": {
"currencyCode": "USD",
"centAmount": 81125
},
...
}],
"customLineItems": [{
"totalPrice": {
"currencyCode": "USD",
"centAmount": 1000
},
"id": "3a6c26f8-1809-474b-b113-7f06147567b3",
"name": {
"en": "Mandatory Insurance for Items above $500"
},
"money": {
"currencyCode": "USD",
"centAmount": 1000
},
"slug": "mandatory-insurance",
"quantity": 1,
"discountedPricePerQuantity": [],
"taxCategory": {
"typeId": "tax-category",
"id": "af6532f2-2f74-4e0d-867f-cc9f6d0b7c5a"
}
}],
...
}

If we remove the Line Item again, the Custom Line Item is also removed:

{
"version": 1,
"actions": [
{
"action": "removeLineItem",
"lineItemId": "85f926ba-5bdb-425f-9bf7-d31130dc9502"
}
]
}
{
"lineItems": [],
"customLineItems": [],
...
}

Calling the Extension only when necessary

Currently, our second API Extension will be called during any update or create action to Carts. However, in our validation logic for high-value items, we only care about items costing $500 or more. Our Extension does not need to be called every time any new item is added to the Cart. We will use conditional triggers to help eliminate the unnecessary API calls. This will ease the load on our Extension and improve overall performance.

We will be using the same example as above, but this time we will specify a condition:

{
"destination": {
"type": "HTTP",
"url": "https://example.org/extension"
},
"triggers": [
{
"resourceTypeId": "cart",
"actions": ["Create", "Update"],
"condition": "lineItems(priceAmount(centAmount >= 50000))"
}
]
}

With this conditional statement in place, our Extension will only be called when the condition we defined evaluates to true. In the above case, the Extension is triggered only when an item with a value of $500 or more is added to the Cart.

However, we still have a small issue. This conditional implementation doesn't take into account the logic we have implemented to remove the mandatory insurance when the high-value item is removed from the Cart. A simple way we can fix this is by making the expression more generic. For example, by applying the condition lineItems has changed. Instead of checking for the high-value item specifically, we check for any changes to Line Items. With this condition, our Extension will trigger every time an item is added, removed, or changed in the Cart. The Extension, however, will not be called when any other aspect of the Cart changes, for example when the shipping information is updated. This is still not perfect, but will already reduce some of the unnecessary API calls to the Extension.

To come up with the best conditional statement for our use case, let's break down when exactly we do and do not want our Extension to be called:

  • If a Cart has no Custom Line Items but has at least one Line Item with a value of $500 or more - in this case, we call the Extension to add the insurance
  • If a Cart does have a Custom Line Item but has no Line Items with a value of $500 or more - in this case, we call the Extension to remove the insurance
  • If a Cart does not have a Custom Line Item, nor any Line Items with a value of $500 or more - in this case, we do not call the Extension

The first two statements represent the two different situations in which we want to call the Extension. We have to translate these two statements into a single conditional statement that will evaluate to either true or false.

Let's start with the first statement. We want to express the following condition:

  • The Cart has no Custom Line Items
  • The Cart has at least one Line Item with a value of $500 or more

To translate this into a statement that either evaluates to true or false, we will use the and operator. The statement looks like this: (customLineItems is empty) and lineItems(priceAmount(centAmount >= 50000)).

In our second statement, we want to express the following condition:

  • The Cart has a Custom Line Item
  • The Cart has no Line Items with a value of $500 or more

Just as above, we will use the and operator to express the state: ((customLineItems is not empty) and not(lineItems(priceAmount(centAmount >= 50000))).

Finally, let's combine them to a single condition. To summarize, we want to call our Extension if either the first or the second condition is met. Therefore, we will use the or operator to join our two sub-conditions. Our final condition will look like this: ((customLineItems is empty) and lineItems(priceAmount(centAmount >= 50000))) or ((customLineItems is not empty) and not(lineItems(priceAmount(centAmount >= 50000)))).

By implementing this condition, we can ensure that our Extension is called only when required by our custom use case.

Conclusion

We've learned how to add API Extensions to the Cart API explained on two example implementations: One that validates any changes to the Cart, and another one that updates the Cart according to its current state. We've also demonstrated how conditional triggers can help eliminate unnecessary calls to our Extension.

You can learn more about API Extensions in the documentation of the API endpoint.