Vue Storefront is now Alokai! Learn More
Extending the Middleware

Extending the Middleware

Extensions allow you to customize how the middleware and integrations run. You can add new API endpoints, hook into the request lifecycle, or modify the Express.js application itself.

Quick Start: Adding Custom API Methods

The most common use case is adding custom API methods. Every Alokai project includes a pre-configured custom extension for this purpose.

1

Define types in apps/storefront-middleware/api/custom-methods/types.ts:

types.ts
export interface AddToWishlistArgs {
  productId: string;
  customerId: string;
}

export interface AddToWishlistResponse {
  wishlistId: string;
}

2

Implement your method in apps/storefront-middleware/api/custom-methods/addProductToWishlist.ts:

custom.ts
import { getLogger } from '@alokai/connect/middleware';
import type { IntegrationContext } from '@/types';
import type { AddToWishlistArgs, AddToWishlistResponse } from './types';

export async function addToWishlist(
  context: IntegrationContext,
  args: AddToWishlistArgs,
): Promise<AddToWishlistResponse> {
  const logger = getLogger(context);
  
  const { api } = await context.getApiClient();
  const result = await api.addProductToWishlist({
    customerId: args.customerId,
    product: { id: args.productId },
  });
  
  return { wishlistId: result.wishlistId };
}

3

Export your method in apps/storefront-middleware/api/custom-methods/index.ts:

index.ts
export * from '@/api/custom-methods/addProductToWishlist';
export * from '@/api/custom-methods/types';

Use it in your frontend:

const { wishlistId } = await sdk.customExtension.addToWishlist({
  productId: '123',
  customerId: 'customer-456',
});

Setting Up Custom Extension

If you don't have a custom extension configured yet in your project, see the Extension with API Methods section to learn how to create and register a new extension from scratch.

Working with the Context Object

Every custom method receives a context object as its first parameter:

PropertyDescription
getApiClient()Access integration's API methods (e.g., const { api } = await context.getApiClient())
clientUnderlying HTTP client (e.g., AxiosInstance) for custom requests
configIntegration configuration (e.g., context.config.apiUrl)

Example:

export async function getProductWithReviews(
  context: IntegrationContext,
  args: { productId: string }
) {
  // Use integration methods
  const { api } = await context.getApiClient();
  const product = await api.getProduct({ id: args.productId });
  
  // Make custom HTTP requests
  const reviews = await context.client.get(`/custom/reviews/${args.productId}`);
  
  return { product, reviews: reviews.data };
}

Using Normalizers

When working with the Unified Data Model, use normalizers to transform platform-specific data into the unified format:

import { getNormalizers } from '@vsf-enterprise/unified-api-sapcc';
import type { SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';

export async function getProductWithReviews(
  context: SapccIntegrationContext,
  args: { productId: string }
) {
  const { normalizeProduct } = getNormalizers(context);
  
  const { api } = await context.getApiClient();
  const rawProduct = await api.getProduct({ id: args.productId });
  const product = normalizeProduct(rawProduct);
  
  return { product };
}

Learn More

For custom fields and advanced normalizer usage, see the Normalizers Guide.

Combining Multiple Data Sources

To combine data from multiple integrations (e.g., eCommerce + CMS), use context.getApiClient():

export async function getProductWithContent(
  context: IntegrationContext,
  args: { productId: string }
) {
  const [{ api }, contentful] = await Promise.all([
    context.getApiClient(),
    context.getApiClient('contentful'),
  ]);
  
  const [product, content] = await Promise.all([
    api.getProduct({ id: args.productId }),
    contentful.api.getEntries({ content_type: 'product', 'fields.sku': args.productId }),
  ]);
  
  return { product, content };
}

Data Federation Guide

For type-safe helpers and best practices, see the Data Federation Guide.


Advanced: Creating Custom Extensions

Beyond the built-in custom extension, you can create specialized extensions for caching, logging, or custom middleware behavior.

Define a new extension using the defineIntegrationExtension factory function:

integrations/sapcc/extensions/custom/index.ts
import { defineIntegrationExtension } from '@alokai/connect/integration-kit';
import type { SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';

import * as methods from '@/api/custom-methods';

export const customExtension = defineIntegrationExtension<SapccIntegrationContext>()({
  extendApiMethods: methods,
  isNamespaced: true,
  name: 'custom',
});

Key features:

  • Currying pattern - defineIntegrationExtension<TContext>()({...}) allows TypeScript to infer API method types while you explicitly provide the context type
  • Smart defaults - name defaults to 'custom' and isNamespaced defaults to true
  • Direct property access - Unlike ApiClientExtension, the return type allows direct access like customExtension.extendApiMethods.yourMethod

Basic Extension Example

Create extensions in apps/storefront-middleware/integrations/<integration-name>/extensions/:

1

Create the extension:

extensions/commerce/extensions/logging.ts
import { defineIntegrationExtension } from '@alokai/connect/integration-kit';
import type { SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';

export const loggingExtension = defineIntegrationExtension<SapccIntegrationContext>()({
  name: 'logging',
  isNamespaced: false,
  extendApiMethods: {},
  hooks: (req, res, { logger }) => ({
    beforeCall({ callName, args }) {
      logger.info(`[${callName}] Request started`, { args });
      return args;
    },
    afterCall({ callName, response }) {
      logger.info(`[${callName}] Request completed`);
      return response;
    },
  }),
});

2

Export and register it:

integrations/commerce/extensions/index.ts
export * from './logging';
integrations/commerce/config.ts
import { loggingExtension } from '@/integrations/commerce/extensions';

export const config = {
  // ...
  extensions: (extensions: ApiClientExtension[]) => [
    ...extensions,
    loggingExtension,
  ],
} satisfies Integration<Config>;

Extension with API Methods

To add new API endpoints via an extension, use extendApiMethods:

extensions/storeLocator/index.ts
import { defineIntegrationExtension } from '@alokai/connect/integration-kit';
import type { SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';

export const storeLocatorExtension = defineIntegrationExtension<SapccIntegrationContext>()({
  name: 'storeLocator',
  isNamespaced: true, // Creates /api/commerce/storeLocator/* endpoints
  extendApiMethods: {
    findNearbyStores: async (context, params: { lat: number; lng: number }) => {
      // ... implementation
      return { stores: [], total: 0 };
    },
    checkStoreStock: async (context, params: { storeId: string; sku: string }) => {
      // ... implementation
      return { available: true, quantity: 5 };
    },
  },
});

Using in the Frontend:

1

Export types in extensions/storeLocator/types.ts:

import type { WithoutContext } from '@alokai/connect/middleware';
import type { storeLocatorExtension } from './index';

export type StoreLocatorEndpoints = WithoutContext<typeof storeLocatorExtension['extendApiMethods']>;

2

Create SDK module in sdk/modules/store-locator.ts:

import { defineSdkModule } from '@vue-storefront/next';
import type { StoreLocatorEndpoints } from 'storefront-middleware/types';

export const storeLocator = defineSdkModule(({ buildModule, config, getRequestHeaders, middlewareModule }) =>
  buildModule(middlewareModule<StoreLocatorEndpoints>, {
    apiUrl: `${config.apiUrl}/commerce/storeLocator`,
    ssrApiUrl: `${config.ssrApiUrl}/commerce/storeLocator`,
    defaultRequestConfig: { headers: getRequestHeaders() },
  }),
);

3

Use it:

const { stores } = await sdk.storeLocator.findNearbyStores({ lat: 40.7128, lng: -74.0060 });

Lifecycle Hooks

Extensions can hook into the request lifecycle at four key points:

Middleware Data Flow
HookWhenUse For
beforeCreateOnce, before API client connectionModify configuration, merge defaults
afterCreateOnce, after API client is createdCleanup, log initialization
beforeCallBefore each API method callValidate/transform arguments, add auth tokens
afterCallAfter each API method callTransform responses, set cache headers, log

Real-World Examples

Reusable Cache Extension

Create a configurable caching extension that works across integrations:

integrations/common/extensions.ts
import type { ApiClientExtension } from '@alokai/connect/middleware';

export const createCacheControlExtension = (
  config: Record<string, string>
): ApiClientExtension => ({
  name: 'cache-control',
  isNamespaced: false,
  hooks(req, res) {
    return {
      afterCall({ response, callName }) {
        if (req.method === 'GET' && !res.getHeader('set-cookie') && callName in config) {
          res.set('Cache-Control', config[callName]);
        }
        return response;
      },
    };
  },
});

Usage:

integrations/commerce/config.ts
import { createCacheControlExtension } from '@/integrations/common/extensions';

extensions: (extensions: ApiClientExtension[]) => [
  ...extensions,
  createCacheControlExtension({
    getProduct: 'public, max-age=3600',
    searchProducts: 'public, max-age=1800',
  }),
],

Custom Express Routes

Use extendApp to add custom routes to the Express application:

extensions/health/index.ts
import { defineIntegrationExtension } from '@alokai/connect/integration-kit';

export const healthCheckExtension = defineIntegrationExtension()({
  name: 'health-check',
  isNamespaced: false,
  extendApiMethods: {},
  extendApp: ({ app }) => {
    app.get('/health', (req, res) => {
      res.json({ status: 'ok', timestamp: new Date().toISOString() });
    });
  },
});

Overriding Built-in API Methods

Extensions can override built-in integration methods for legacy compatibility or custom behavior. This is useful when an integration provides multiple API versions and you need to use older endpoints.

Use Case: SAP Commerce Cloud Smartedit Legacy Endpoints

SAP Commerce Cloud supports two types of Smartedit endpoints. Legacy SAPCC customers use getPage and getComponentsByIds, while newer implementations use getPageWithUser and getComponentsByIdsAndUser. The built-in Smartedit unified extension uses the newer methods by default.

To force the unified extension to use legacy endpoints, create a non-namespaced extension that overrides the newer methods:

sf-modules/cms-smartedit/extensions/legacy.ts
import { defineIntegrationExtension } from '@alokai/connect/integration-kit';
import type { SmarteditIntegrationContext } from '@vsf-enterprise/smartedit-api';

export const legacyExtension = defineIntegrationExtension<SmarteditIntegrationContext>()({
  name: 'legacy',
  isNamespaced: false,
  extendApiMethods: {
    async getComponentsByIdsAndUser(context, ...params) {
      const { api } = await context.getApiClient();
      return api.getComponentsByIds(...params);
    },
    async getPageWithUser(context, ...params) {
      const { api } = await context.getApiClient();
      return api.getPage(...params);
    },
  },
});

Type Safety Required

When overriding built-in methods, always match the original method signature to avoid runtime errors. Using defineIntegrationExtension<IntegrationContext>() provides TypeScript compile-time verification that your override methods match the original contract.

Multi-Store Configuration

For dynamic configuration switching based on requests (e.g., path-based multi-store setups), see the Config Switcher Guide.


TypeScript Support

For full type safety and autocomplete support, use defineIntegrationExtension from @alokai/connect/integration-kit. This factory function provides automatic type inference for API methods while allowing you to specify the integration context type.

The factory uses a currying pattern that allows TypeScript to infer your API method types automatically:

integrations/commerce/extensions/checkout/index.ts
import { defineIntegrationExtension } from '@alokai/connect/integration-kit';
import type { SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';

import { createPaymentAndPlaceOrder } from '@/api/checkout/createPaymentAndPlaceOrder';

export const checkoutExtension = defineIntegrationExtension<SapccIntegrationContext>()({
  name: 'checkout',
  isNamespaced: true,
  extendApiMethods: {
    createPaymentAndPlaceOrder,
  },
  hooks: (req, res, { logger }) => ({
    beforeCreate({ configuration }) {
      // configuration is properly typed as SapccIntegrationContext["config"]
      logger.info('Initializing checkout with API URL:', configuration.api);
      return configuration;
    },
  }),
});

Key differences from ApiClientExtension:

  • Only requires one generic parameter (TContext) instead of three
  • Configuration type is automatically extracted from TContext["config"]
  • API methods type is inferred from extendApiMethods
  • The hooks function receives an additional hooksContext parameter with logger

Using Factory Functions for API Methods

You can pass a factory function to extendApiMethods that receives the integration context, useful when methods need access to configuration at definition time:

import { defineIntegrationExtension } from '@alokai/connect/integration-kit';

type CustomContext = { config: { apiUrl: string } };

export const apiExtension = defineIntegrationExtension<CustomContext>()({
  name: 'api',
  extendApiMethods: (context) => ({
    getApiUrl: async () => ({ url: context.config.apiUrl }),
  }),
});

Alternative: Using ApiClientExtension Directly

For cases where you need more control, you can still use ApiClientExtension with three generic parameters:

ApiClientExtension<Endpoints, IntegrationContext, Config>
integrations/commerce/extensions/checkout/index.ts
import type { ApiClientExtension } from '@alokai/connect/middleware';
import type {
  ApiMethods,
  MiddlewareConfig,
  SapccIntegrationContext,
} from '@vsf-enterprise/sapcc-api';

export const checkoutExtension = {
  name: 'checkout',
  isNamespaced: true,
  extendApiMethods: {
    // ...methods
  },
  hooks(req, res) {
    return {
      beforeCreate({ configuration, logger }) {
        logger.info('Initializing checkout with API URL:', configuration.api);
        return configuration;
      },
    };
  },
} satisfies ApiClientExtension<ApiMethods, SapccIntegrationContext, MiddlewareConfig>;

Benefits

✅ With defineIntegrationExtension:

  • Automatic API method type inference
  • Direct property access on extendApiMethods (e.g., extension.extendApiMethods.yourMethod)
  • Simpler generic requirements (only TContext needed)
  • Access to logger in hooks via hooksContext parameter

✅ With Proper Generics (either approach):

  • Full autocomplete for configuration properties in hooks
  • Type-safe context parameter in extendApiMethods
  • Catch configuration errors at compile time

❌ Without Type Information:

  • No autocomplete support
  • Runtime errors from typos in configuration access
  • No validation of return types

API Reference

defineIntegrationExtension

Factory function for creating type-safe integration extensions.

Import:

import { defineIntegrationExtension } from '@alokai/connect/integration-kit';

Signature:

defineIntegrationExtension<TContext>()({
  name?: string,           // Default: 'custom'
  isNamespaced?: boolean,  // Default: true
  extendApiMethods: TApiMethods | ((context: TContext) => TApiMethods),
  extendApp?: ExtendAppFunction,
  hooks?: (req, res, hooksContext) => HookHandlers,
}): IntegrationExtension<TApiMethods, TName, TIsNamespaced>

Parameters:

ParameterTypeDefaultDescription
namestring'custom'Unique identifier for the extension
isNamespacedbooleantrueIf true, methods are at /{integration}/{extension}/{method}
extendApiMethodsobject | functionRequiredAPI methods object or factory function receiving context
extendAppfunction-Receives Express.js app instance for adding routes/middleware
hooksfunction-Returns lifecycle hook handlers. Receives (req, res, hooksContext)

Extension Properties (Legacy)

When using ApiClientExtension directly:

PropertyTypeDescription
namestringUnique identifier for the extension
isNamespacedbooleanIf true, methods are at /{integration}/{extension}/{method} (prevents collisions)
If false (default), methods are at /{integration}/{method}
extendApiMethodsobjectCustom API methods. Each receives (context, params) and returns a Promise
extendAppfunctionReceives Express.js app instance for adding routes/middleware
hooksfunctionReturns lifecycle hook handlers. Receives (req, res) parameters

Hook Parameters

When using defineIntegrationExtension, the hooks function receives three parameters:

  • req - The Express request object (AlokaiRequest)
  • res - The Express response object (AlokaiResponse)
  • hooksContext - Container with shared services including logger

All hook handlers receive these common parameters:

  • configuration - Integration configuration object (typed as TContext["config"])
  • callName - Name of the API method being called
  • args - Arguments array passed to the method
  • response - Response returned by the method (afterCall only)
  • logger - Logger instance (use instead of console.log)

Logging Best Practices

For structured logging, configuration, and best practices, see the Logging Guide.


Best Practices & Common Pitfalls

✅ Always Return from Hooks

Hooks must return modified values:

// ❌ BAD
export const myExtension = defineIntegrationExtension()({
  name: 'my-extension',
  extendApiMethods: {},
  hooks: (req, res) => ({
    beforeCall({ args }) {
      args[0].timestamp = Date.now();
      // Missing return statement!
    },
  }),
});

// ✅ GOOD
export const myExtension = defineIntegrationExtension()({
  name: 'my-extension',
  extendApiMethods: {},
  hooks: (req, res) => ({
    beforeCall({ args }) {
      return [{ ...args[0], timestamp: Date.now() }];
    },
  }),
});

✅ Use Namespacing to Prevent Collisions

// ⚠️ RISK: Might override existing methods
export const customExtension = defineIntegrationExtension()({
  name: 'custom',
  isNamespaced: false, // /commerce/getProduct - collision risk!
  extendApiMethods: {
    getProduct: async (context, params) => { /* ... */ },
  },
});

// ✅ SAFE: Separate namespace (default behavior)
export const customExtension = defineIntegrationExtension()({
  name: 'custom',
  // isNamespaced defaults to true: /commerce/custom/getProduct - no collision
  extendApiMethods: {
    getProduct: async (context, params) => { /* ... */ },
  },
});

✅ Keep Hooks Lightweight

Hooks run on every request - avoid expensive operations:

// ❌ BAD: Blocks every request
export const myExtension = defineIntegrationExtension()({
  name: 'my-extension',
  extendApiMethods: {},
  hooks: (req, res) => ({
    beforeCall({ args }) {
      const data = expensiveSyncOperation(); // Runs on every request!
      return args;
    },
  }),
});

// ✅ GOOD: One-time setup
export const myExtension = defineIntegrationExtension()({
  name: 'my-extension',
  extendApiMethods: {},
  hooks: (req, res) => ({
    beforeCreate({ configuration }) {
      configuration.cachedData = expensiveOperation(); // Runs only once
      return configuration;
    },
  }),
});

✅ Parallelize Data Federation

// ❌ Sequential: 600ms total
const commerceApi = await commerce.getApiClient();
const product = await commerceApi.api.getProduct({ id });
const cmsApi = await cms.getApiClient();
const content = await cmsApi.api.getContent({ sku: id });

// ✅ Parallel: 300ms total
const [commerceApi, cmsApi] = await Promise.all([
  commerce.getApiClient(),
  cms.getApiClient(),
]);
const [product, content] = await Promise.all([
  commerceApi.api.getProduct({ id }),
  cmsApi.api.getContent({ sku: id }),
]);

✅ Use Type Safety

import { defineIntegrationExtension } from '@alokai/connect/integration-kit';
import type { SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';

// Recommended: Use defineIntegrationExtension for automatic type inference
export const myExtension = defineIntegrationExtension<SapccIntegrationContext>()({
  name: 'my-extension',
  extendApiMethods: {
    // Methods are automatically typed
  },
});

✅ Use Structured Logging

Always use the logger parameter instead of console.log for proper log aggregation and filtering.