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:
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:
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:
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:
| Property | Description |
|---|---|
api | Integration's API methods (e.g., context.api.getProduct()) |
client | Underlying HTTP client (e.g., AxiosInstance) for custom requests |
config | Integration 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:
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:
export * from './logging';
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:
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:
| Hook | When | Use For |
|---|---|---|
| beforeCreate | Once, before API client connection | Modify configuration, merge defaults |
| afterCreate | Once, after API client is created | Cleanup, log initialization |
| beforeCall | Before each API method call | Validate/transform arguments, add auth tokens |
| afterCall | After each API method call | Transform responses, set cache headers, log |
Real-World Examples
Reusable Cache Extension
Create a configurable caching extension that works across integrations:
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:
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:
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:
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
configurationproperties in hooks - Type-safe
contextparameter inextendApiMethods - 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
| Property | Type | Description |
|---|---|---|
name | string | Unique identifier for the extension |
isNamespaced | boolean | If true, methods are at /{integration}/{extension}/{method} (prevents collisions)If false (default), methods are at /{integration}/{method} |
extendApiMethods | object | Custom API methods. Each receives (context, params) and returns a Promise |
extendApp | function | Receives Express.js app instance for adding routes/middleware |
hooks | function | Returns lifecycle hook handlers. Receives (req, res) parameters |
Hook Parameters
All hooks receive these common parameters:
configuration- Integration configuration objectcallName- Name of the API method being calledargs- Arguments array passed to the methodresponse- Response returned by the method (afterCall only)logger- Logger instance (use instead ofconsole.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.