Enhance your commercetools API with Deno

A Guide to Enhancing commercetools API Extensions with Deno

Willem Haring
Willem Haring
Sales Engineer, commercetools
Published 16 February 2024
Estimated reading time minutes

The commercetools API provides a way to extend the behavior of an API with your own business logic. For instance, when you want to check if the content of a cart is valid, you can execute your own logic on the creation or update a cart. That update will be executed before the cart object is actually committed, making it an ideal solution for validation, additions, etc. In this article, I will describe how you can use API extensions with Deno, and how to run them from your laptop for demo or POC purposes.

Enhance your commercetools API with Deno

API extensions

Within commercetools, you can register an API extension on the following resources: Carts, Orders, Payments, Customers, Quote Requests, Staged Quotes, Quotes and Business Units. The API extension can be triggered on the Create or Update of one of these resources. When you register an API extension for a resource, you will need to provide a destination address that will be called when the API extension is triggered. The destination can be an HTTPDestination, a GoogleCloudFunctionDestination or an AWSLambdaDestination. In this article, I will use the HTTPDestination that will point to a web server that I will host on my laptop.

API extensions

When the cart extension is registered, it will call the extension every time when an update is made to the cart resource, regardless if the update is made through the API or, for instance, the Merchant Center. The called HTTP endpoint will receive the action and should reply with a statuscode of 200 to indicate that the validation succeeded. commercetools will then commit the cart object. If the response is a statuscode of 400, the transaction will be rolled back and an error will be thrown.

Merchant Center

Registering a web server running on your laptop

With Deno, it's really easy to start a web server to listen to incoming requests using the Oak middleware:

import { Application } from"https://deno.land/x/oak/mod.ts";

const app = new Application();

app.use((ctx) => {
   ctx.response.body = "Hello World!";
});
await app.listen({ port: 8000 });
const handle = importsdk.init()

When you run this with deno run --allow-net helloWorld.ts  you have a web server that listens to any incoming request on port 8080 and will respond with a “hello world!” message.

But when you want to register an API extension in commercetools, that HTTP endpoint needs to be publicly accessible. Luckily there is an application called NGROK that can help. You can install NGROK locally on your machine (see: https://ngrok.com/download). Install NGROK, create an authentication token and run it using:

ngrok http 8080

This will create a public HTTP address that forwards all incoming requests to it on port 8080 running on your machine.

To make this work a bit simpler, I created a small wrapper class that starts NGROK as a subprocess and gives you the public URL using the NGROK API. The only thing needed are the following two lines in your .env file:

NGROK_API_KEY=************************************
NGROK_API_ENDPOINT=https://api.ngrok.com/endpoints

The API key can be retrieved here: https://dashboard.ngrok.com/api 

With NGROK installed and running we can proceed to the next step: Registering and unregistering the API extension.

Registering and unregistering the API extension

The whole process of registering an API extension is very well described in the API documentation of commercetools.

I created two methods for registering and unregistering:

async registerExtensions(destination: string, triggers: iTriggers[]) {
        for (const trigger of triggers) {
          const draft: ExtensionDraft = {
            key: `trigger-${hash(trigger)}`,
            destination: {
              type: "HTTP",
          url: destination
            },
            triggers: [
              {
                resourceTypeId: trigger.resource,
                actions: trigger.actions
              }
            ]
          }
          await this.registerExtension(draft)
    }    
}

This function takes a list of triggers and the destination, and creates the ExtensionDraft for it. The real registration for each trigger happens here (simplified for readability):

async registerExtension(draft: ExtensionDraft): Promise {
      const ext = await this._handle
          .root()
          .extensions()
          .post({body: draft})
          .execute()

      console.log(`registered extension with key ${ext.body.key}`)
      return ext.body
}

For unregistering:

async unregister(): Promise {
        console.log(`unregister::ApiExtension`)
        await this.unregisterExtensions(this._triggers)
        return true
}
async unregisterExtensions(triggers: iTriggers[]) {
        for (const trigger of triggers) {
          const key = `trigger-${hash(trigger)}`
          const existing = await this._handle
                .root()
                .extensions()
                .withKey({key: key})
                .get()
                .execute()

             await this ._handle
                .root()
                .extensions()
                .withKey({key: key})
                .delete({queryArgs: {version: existing.body.version}})
              .execute()
           console.log(`unregistered extension with key ${existing.body.key}`)
      }
    }

I have put all these functions in a class to handle these.

The array of triggers, passed into these methods, have the following interface:

export type ResourceType = "cart" | "order" | "payment" | "customer" | "quote-request" | "staged-quote" | "quote" | "business-unit" | "product"
export type ActionType = "Create" | "Update"

export interface iTriggers {
  resource: ResourceType
  actions: ActionType[]
  handler?: BaseHandler
}

The base handler is a handler that will handle all messages, and take care of dispatching the right functions when a message is received. It will also take care of the various actions a specific handler might want to perform.

Running the API listener

The API listener can now be run using the following example:

listenexample.ts
import {iCustomerMessage, iCustomerResponse, responseCode, CustomerHandler, apilistener, iTriggers } from
"https://deno.land/x/commercetools_demo_sdk/listener.ts";

class customerLogMessage {
  static async handle(msg: iCustomerMessage): Promise {
    const customer = msg.resource.obj
    console.log(`incoming customer ${msg.action} message for customer with email:${customer.email}`);
    return {
      result: {
        code: responseCode.SUCCESS
      }
    }
  }
}

const listento: iTriggers[] = [
  {
    actions: ["Create", "Update"],
    resource: "customer",
    handler: new CustomerHandler().add(customerLogMessage)
  }
]

const listener = new apilistener()
await listener.listen(listento)
console.log('press any key to continue')
Deno.exit()

When running this application with:

deno run -A listenexample.ts

The following can be seen in the terminal:

Ngrok::constructor
constructor::Proxy
constructor::ApiExtension
constructor::ApiExtensionsListener
init::ApiExtensionsListener
init::Proxy
NGROK::init
starting NGROK
NGROK started
register::ApiExtension
registering API extensions for commercetools project: wh-******
create, update on customer
sending to https://aa832cdaf8b6-*********97725862.ngrok-free.app/listener
registered extension with key trigger-6df399c7c4b38757dbe2da268feba64e49aa080c
listen::ApiExtensionsListener
Listening to api extensions on: https://aa832cdaf8b6-*********97725862.ngrok-free.app
constructor::ListenServer
constructor::extensionHandler
press ^c to terminate

When you make a change to a customer, the following will appear on the screen:

incoming customer Update message for customer with key: de-***-buick

Here we can see, step by step, what is happening:

const listener = new apilistener()

This creates a new API listener class that is able to listen to registered triggers. The triggers are registered with the following array of triggers:

const listento: iTriggers[] = [
  {
    actions: ["Create", "Update"],
    resource: "customer",
    handler: new CustomerHandler().add(customerLogMessage)
  }
]

This one trigger will listen to Create and Update messages on the customer resource. An incoming message will be handled by a CustomerHandler. To this customer handler, new sub handlers can be added. In this case, a customerLogMessage handler, is doing the following:

const myCustomerHandler = {
  handle: async (msg: iCustomerMessage): Promise => {
    const customer = msg.resource.obj
    console.log(`incoming customer ${msg.action} message for customer with email:
${customer.email}`);
  return {
      result: {
       code: responseCode.SUCCESS
      }
    }
  }
}

The handle method is called by the CustomerHandler and is returning a responseCode.SUCCSESS in this instance. All the typing for the resources is handled by iCustomerMessage and iCustomerResponse, so that intellisense is fully working in vscode.

Multiple handlers can be chained together so that multiple pieces of business logic can be executed on an API extension. The following example demonstrates setting a customer number on the customer object when it is not already set:

export class generateCustomerNumber {
  static async handle(msg: iCustomerMessage): Promise {
    const customer = msg.resource.obj

    if (customer.customerNumber === undefined) {
      console.log(`setting customer number for customer with email: ${customer.email}`);
      return {
        result: {
          code: responseCode.SUCCESS,
          actions: [
            {
              action: "setCustomerNumber",
              customerNumber: await generateNumber(customer.id)
            }
          ]
        }
      }
    }
    return {
      result: {
        code: responseCode.SUCCESS
      }
    }

  }
}

const generateNumber = async (id: string): Promise => {
  await delay(100) // just to mock a bit of delay…
  return hash(id)
}

This extra function can be chained to the CustomerHandler in the following way:

const listento: iTriggers[] = [
  {
    actions: ["Create", "Update"],
    resource: "customer",
    handler: newCustomerHandler().add(myCustomerHandler).add(generateCustomerNumber)
  }
]

When creating a new customer in commercetools, the following will now be shown in the terminal window:

incoming customer Create message for customer with email: new@customer.org
setting customer number for customer with email: new@customer.org

The responses from the various sub handlers are all chained together. When one fails, all fail and a 400 is returned to commercetools. When multiple actions are returned, they are returned as one big action array.

This can really speed up the process of testing and validating API extensions with commercetools in a demo or POC scenario. In another of my following articles, I will use this module to build a couple of useful business scenarios that will illustrate the use of API extensions with real-life examples.

To learn about streamlining development with the Deno SDK, read my blog post A Deno SDK for commercetools.

Willem Haring
Willem Haring
Sales Engineer, commercetools

Willem has been working in retail technology since the beginning of 2000. He has worked with several technology companies and with clients on point of sales, stores infrastructure, reporting and dashboarding. He has been a participating member on the ARTS committee for the data warehouse chapter under ARTS XML. At commercetools, Willem works with clients, prospects and customers to translate complicated use cases to workable eCommerce solutions based on commercetools.

Related Blog Posts