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 result = await context.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
apiIntegration's API methods (e.g., context.api.getProduct())
clientUnderlying HTTP client (e.g., AxiosInstance) for custom requests
configIntegration configuration (e.g., context.config.apiUrl)
getApiClient()Access other integrations for data federation

Example:

export async function getProductWithReviews(
  context: IntegrationContext,
  args: { productId: string }
) {
  // Use integration methods
  const product = await context.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 rawProduct = await context.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 contentful = await context.getApiClient('contentful');
  
  const [product, content] = await Promise.all([
    context.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.

Basic Extension Example

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

1

Create the extension:

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

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

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 type { ApiClientExtension } from '@alokai/connect/middleware';

export const storeLocatorExtension = {
  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 };
    },
  },
} satisfies ApiClientExtension;

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 type { ApiClientExtension } from '@alokai/connect/middleware';

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

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, you should pass proper generic types to ApiClientExtension. This ensures that hooks like beforeCreate receive correctly typed configuration parameters, and your extension methods get proper type inference.

Typing Extensions

ApiClientExtension accepts three generic parameters:

ApiClientExtension<Endpoints, IntegrationContext, Config>

Each integration package exports these types. Here's a real-world example for SAP Commerce Cloud:

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

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

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

Benefits

✅ With Proper Generics:

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

❌ Without Generics:

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

API Reference

Extension Properties

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

All hooks receive these common parameters:

  • configuration - Integration configuration object
  • 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 = {
  name: 'my-extension',
  hooks(req, res) {
    return {
      beforeCall({ args }) {
        args[0].timestamp = Date.now();
        // Missing return statement!
      },
    };
  },
} satisfies ApiClientExtension;

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

✅ Use Namespacing to Prevent Collisions

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

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

✅ Keep Hooks Lightweight

Hooks run on every request - avoid expensive operations:

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

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

✅ Parallelize Data Federation

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

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

✅ Use Type Safety

export const myExtension = {
  // ... config
} satisfies ApiClientExtension;

✅ Use Structured Logging

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