Vue Storefront is now Alokai! Learn More
Storefront Extension

Storefront Extension

Learn how to customize the Unified Data Layer through normalizers and API methods

Storefront and the Unified Data Layer offer various extension points that allow you to customize and modify its behavior according to your needs.

Some of these extension points include:

  • Adding fields to Normalizers
  • Overriding Normalizers
  • Overriding API Methods
  • Adding API Methods
  • Orchestrating Data with API Methods

All of these functionalities involve changes to your Unified API Extension in the middleware.config.ts file.

Customizing Normalizers

Normalizers transform the data from your eCommerce backend into a unified format. For each integration, there is a set of default normalizers that convert the raw data into the base Unified Data Model.

However, if you need additional fields not included by the default normalizers, you can add them by extending or writing the existing normalizers.

In your middleware.config.ts file, you can override the default normalizers by using the defineNormalizers function from your Unified API integration. This function allows your custom normalizers to be typed correctly.

middleware.config.ts
import {
  type Context,
  methods,
  normalizers as integrationNormalizers,
  createUnifiedExtension,
  defineNormalizers,
} from "@vsf-enterprise/unified-api-[Integration]";

/* 
  Passing <typeof integrationNormalizers> will provide 
  proper typing for the arguments of the default normalizers 
*/
const normalizers = defineNormalizers<typeof integrationNormalizers>()({
  // pass the default normalizers
  ...integrationNormalizers,
  // override any of the normalizers
  normalizeCart: (input, ctx) => { 
    // Do your normalization logic here
  },
  normalizeProduct: (input, ctx) => {},
  normalizeCustomer: (input, ctx) => {},
});

export const unifiedApiExtension = createUnifiedExtension<Context>()({
  // use our customized set of normalizers in our Unified API extension
  normalizers,
  apiMethods: {
    ...methods<typeof normalizers>(),
  },
});

// ...

Adding Fields to Normalized Data

If you want to keep the existing Unified fields, but want to pass additional data from your eCommerce backend, you can extend the existing normalizers. The input argument will contain more data coming from your eCommerce backend. When normalizing, some of this data is dropped from the response, but you can use a custom normalizer to include additional fields.

For this, you can can override a normalizer, call the default normalizer with the same name, and then add any additional fields to the returned object.

middleware.config.ts
import {
  type Context,
  methods,
  normalizers as integrationNormalizers,
  createUnifiedExtension,
  defineNormalizers,
} from "@vsf-enterprise/unified-api-[Integration]";

const normalizers = defineNormalizers<typeof integrationNormalizers>()({
  // pass the default normalizers
  ...integrationNormalizers,
  // override any of the normalizers
  normalizeCart: (input, ctx) => ({ 
      // return the default normalized data
      ...integrationNormalizers.normalizeCart(input, ctx),
      code: input.code,
      customField: input.customField,
  }),
  normalizeProduct: (input, ctx) => { /* ... */ },
  normalizeCustomer: (input, ctx) => { /* ... */ },
});

export const unifiedApiExtension = createUnifiedExtension<Context>()({
  // use our customized set of normalizers in our Unified API extension
  normalizers,
  apiMethods: {
    ...methods<typeof normalizers>(),
  },
});

Overriding Default Normalizers

In some cases, you may want to completely replace the default normalizers with your own. This follows the same pattern as before, but instead of using the default normalizers, you'll provide your own implementation.

Your normalizer functions take the raw data from your eCommerce backend and you can transform it into any shape you need, including/excluding fields as necessary.

middleware.config.ts
import {
  type Context,
  methods,
  normalizers as integrationNormalizers,
  createUnifiedExtension,
  defineNormalizers,
} from "@vsf-enterprise/unified-api-[Integration]";

const normalizers = defineNormalizers<typeof integrationNormalizers>()({
  // pass the default normalizers
  ...integrationNormalizers,
  // override any of the normalizers
  normalizeCart: (input, ctx) => ({
    name: input.name,
    description: input.description
  }),
  normalizeProduct: (input, ctx) => { /* ... */ },
  normalizeCustomer: (input, ctx) => { /* ... */ },
});

export const unifiedApiExtension = createUnifiedExtension<Context>()({
  // use our customized set of normalizers in our Unified API extension
  normalizers,
  apiMethods: {
    ...methods<typeof normalizers>(),
  },
});

In this example, the normalizeCart function will return only the name and description fields from the raw data, and the rest of the fields will not be included in the response.

If you're looking for a starting point, you can find the default normalizers for your integration in the Docs sidebar.

Customizing API Methods

Another way to customize the Unified Data Layer is by adding new API methods, overriding existing ones, or intercepting the response. This can be useful when you want to modify the default behavior, create new API methods.

This can be done by adding additional methods to the apiMethods object in the unifiedApiExtension configuration. If you want to override an existing method, you can provide a new implementation with the same name. If you want to add a new method, you can add a method with a unique key to the apiMethods object.

middleware.config.ts
import {
  type Context,
  methods,
  normalizers as integrationNormalizers,
  createUnifiedExtension,
  defineNormalizers,
} from "@vsf-enterprise/unified-api-[Integration]";

const normalizers = defineNormalizers<typeof integrationNormalizers>()({
  ...integrationNormalizers,
  // any normalizer customization
});

export const unifiedApiExtension = createUnifiedExtension<Context>()({
  normalizers,
  apiMethods: {
    // include the default API methods
    ...methods<typeof normalizers>(),
    // override or add new API methods
    getProductDetails: (context, args) => {
      // your code here
    },
    newMethod: (context, args) => {

    }
  },
});

// ...

Overriding Default API Methods

If you want to override an existing API method, you can provide a new implementation with a key that matches an existing API method. This will completely replace the default logic of the method.

middleware.config.ts
export const unifiedApiExtension = createUnifiedExtension<Context>()({
  normalizers,
  apiMethods: {
    // include the API methods
    ...methods<typeof normalizers>(),
    // override or add new API methods
    getProductDetails: (context, args) => {
      // your code here
    },
    // ...
  },
});

Your custom API implementation must have the same parameters as the original

When overwriting an API method, the context and args parameters must be the same types as the original method. If you're using the createUnifiedExtension function, the types will be inferred automatically and any errors will be thrown if you incorrectly change a type.

Extending API Methods

In some cases, you may want to preserve and extend the original behavior of an API method. For example, if you want to maintain the original data, but add data from another service, you can extend the response.

You can do this by calling the original method. Then, you can (optionally, use the normalizer available in the context to normalize the original method's response. Finally, you can add any additional fields or customizations to the returned object.

middleware.config.ts
export const unifiedApiExtension = createUnifiedExtension<Context>()({
  normalizers,
  apiMethods: {
    // include the API methods
    ...methods<typeof normalizers>(),
    // override or add new API methods
    getProductDetails: async (context, args) => {
      const response = await context.api.getProduct({ id: args.id });
      const { normalizeProduct } = context.config.normalizers;
      const product = response;

      return {
        ...normalizeProduct(product, { locale: "en", currency: "USD" }),
        availableForPickup: product.availableForPickup,
      };
    }
    // ...
  },
});

Alternatively, you can store the original Unified methods in a separate variable and use them in your custom implementation. This can be helpful if you want to keep the original Unified behavior, combine multiple Unified method calls, or use the original method as a fallback.

middleware.config.ts
import { type Context, methods, normalizers, createUnifiedExtension } from "@vsf-enterprise/unified-api-[Integration]";

// Store the original Unified methods
const unifiedApiMethods = methods<typeof normalizers>();

export const unifiedApiExtension = createUnifiedExtension<Context>()({
  normalizers,
  apiMethods: {
    ...unifiedApiMethods,
    getProductDetails: async (context, args) => {
      // use the original method
      const response = await unifiedApiMethods.getProductDetails(context, args);
      // customize the response
      return { ...response, someNewProperty: "My new property" };
    },
  },
});

This pattern is also useful for intercepting requests. If you want to maintain the same behavior, but add custom logging, analytics, event tracking, or any other behavior, you can use the original method with same response. But run additional code before or after the original method is called.

Adding New API Methods

If you want to add a new API method, you can provide a new implementation with a unique key in the apiMethods object. This will add a new method to the Unified Data Layer.

To ensure proper typing, you can create a new interface for the args parameter and return type of your new method. This will be used to typecheck the arguments when you call the method from the SDK and can be helpful when following our Next.js/Nuxt data fetching patterns.

middleware.config.ts

export interface GetHomepageArgs {
  // your arguments
}

export interface GetHomepageResponse {
  data: string
}

export const unifiedApiExtension = createUnifiedExtension<Context>()({
  normalizers,
  apiMethods: {
    // include the API methods
    ...methods<typeof normalizers>(),
    // override or add new API methods
    getHomepage: async (context, args: GetHomePageArgs): GetHomePageResponse => {
      return {
        data: "Homepage data"
      }
    }
    // ...
  },
});

Now, thanks to the SDK synchronization, the getHomepage method will be available and typed when you use the SDK in your Storefront.

// Frontend Project
const { data } = sdk.unified.getHomepage(/* args */)

args should be an object

Under the hood, Alokai will transform requests from the SDK to inject the context object into your API methods. By making args an object, not only does ensure that your request will not break during transformation, but you can add new fields to the method in the future without breaking the existing implementation.

Where to Add Customizations?

Before you add your custom behavior on top of the Unified Data Layer, you must first decide if it is best suited on the normalizer or API Method level.

This decision depends heavily on your specific use case, but some general guidelines that may help are:

  • Normalizers are best when the raw input has all of the data you need. If you only want to modify how data
  • API Methods work well when you want to add additional data that isn't available in your normalizer. For example, if you want to add custom fields that aren't coming from your eCommerce backend
  • API Methods are a good choice if you want to only impact the return value of one method

Normalizers may impact multiple API Methods

Normalizers may be used to normalize data in multiple methods. For example, changing the normalizeCart will change the return value of Unified Methods like getCart, addCartLineItem, and removeCartLineItem.

SDK Type Inference

The Unified Data Layer is designed to be type-safe. As you make changes to the unifiedApiExtension, your Storefront project will automatically pass these changes through to the SDK. In the types/ directory, there are inferred types (like SfProduct, SfCart, and SfCustomer) and the relevant code that it comes from.

By default, these types are the same as the standard Unified Data Model, but as you customize your unifiedApiExtension, these types will be updated to reflect the changes.

For example, this is the default type declaration for SfProduct in your Storefront project:

types/unified-data-model/product.ts
export type SfProduct = InferSdk<'getProductDetails'>['product'];

If return values of productNormalizer or getProductDetails is modified, the SfProduct type will be updated to reflect the new return value.

The following updates can change the types in your project:

  • Changing the return value of a normalizer
  • Adding or removing fields from a normalizer
  • Changing the return value of an API method
  • Adding new API methods

Orchestrating Data

Data Orchestration allows you to consolidate multiple server requests into a single endpoint. For example, if you want to fetch data from your CMS, query your eCommerce backend, and return a single response containing all the information.

When creating/overriding an API method, you can query multiple services using the getApiClient method. This method will search your integrations defined in your Middleware Configuration, find one matching a specific key, and return that integration's API client.

For example, we can use Data Orchestration to add additional product information from our CMS when using getProductDetails.

import { type Context, methods, normalizers, createUnifiedExtension } from "@vsf-enterprise/unified-api-[Integration]";

const unifiedApiMethods = methods<typeof normalizers>();

export const unifiedApiExtension = createUnifiedExtension<Context>()({
  normalizers,
  apiMethods: {
    ...unifiedApiMethods,
    getProductDetails: async (context, args) => {
      const response = await unifiedApiMethods.getProducts(context, args);
      const { id } = args;
      // fetch data from your CMS integration 
      const productInfo = await context.getApiClient("cms").getProductInfo(id);

      return { ...response, productInfo };
    },
  },
});

const config = {
  integrations: {
    // cms matches the string passed to `getApiClient`
    cms: {
      location: "@vsf-enterprise/[cms-integration]-api/server",
      //...
    },
    // ...
  },
};