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 the cart. The second one will add a mandatory insurance if any item in the cart is worth $500 or more.

Both 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 by the commercetools platform during the execution of API calls it has 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"
}

And 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"]
}
]
}

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 after the commercetools platform has executed its business logic. 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: '76ff51d8-fe66-4266-b9d7-b0730c95f2c3',
obj:
{ id: '76ff51d8-fe66-4266-b9d7-b0730c95f2c3',
version: 1,
createdAt: 1970-01-01T00:00:00.000Z,
lastModifiedAt: 1970-01-01T00:00:00.000Z,
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 it's approval), the objects are identical.

Validate maximum number of ten items in the cart

Let's try to do something useful with the API Extension! In the first example, we'll validate that carts can not reach a state that we won't allow to order. In the example, 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 into the cart.",
"errors": [
{
"code": "InvalidInput",
"message": "You can not put more than 10 items into 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 an API extension and not inside the commercetools platform. This object contains identifiers for the API Extension that produced the error message helping us to analyze the error.

Add an extra item to the cart

In the second example, we're looking for a valuable item in the cart - defined as a single item being worth $500 or more - and, if one exists, 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": [],
...
}

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 it's current state.

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