Data Federation
Data federation is a technique that allows you to combine multiple API requests into a single endpoint. This guide shows you how to use the getApiClient method to access and combine data from different integrations within your custom methods.
Why Use Data Federation?
- Minimized Network Traffic: Combining multiple server calls into one reduces latency and improves responsiveness for your users.
- Simplified Frontend Logic: Your frontend code becomes cleaner and less complex when you group related requests into a single endpoint.
- Data Unification: Retrieve data from multiple sources (commerce, CMS, legacy systems) and return a unified response that combines information from all of them.
Basic Usage
To access an integration, you need to pass its key to the context.getApiClient() method. The integration key must match the key you defined in your middleware.config.ts:
export const config = {
integrations: {
contentful: contentfulConfig, // key: "contentful"
commerce: commerceConfig, // key: "commerce"
},
}
With these keys defined, you can now access the integrations using context.getApiClient("contentful") or context.getApiClient("commerce").
Here's how to combine data from multiple sources in a single method:
import { type SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';
import { type Endpoints as ContentfulEndpoints } from '@vsf-enterprise/contentful-api';
export async function getPdp(
context: SapccIntegrationContext,
params: { id: string },
) {
const contentful = await context.getApiClient<ContentfulEndpoints>("contentful");
const [product, content] = await Promise.all([
context.api.getProduct({ id: params.id }),
contentful.api.getEntries({
content_type: "product",
"fields.sku": params.id,
})
]);
return { product, content };
}
Real-World Example
A common use case is enriching product data with information from a legacy system. For example, you might want to fetch product details from your commerce backend and combine them with real-time stock information from a separate inventory system.
import { type SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';
import { type Endpoints as LegacySystemEndpoints } from '../../types';
export async function getEnrichedProduct(
context: SapccIntegrationContext,
params: { productId: string },
) {
// Access the legacy system using getApiClient
const legacySystem = await context.getApiClient<LegacySystemEndpoints>("legacySystem");
// Fetch data from both systems concurrently
const [stock, product] = await Promise.all([
legacySystem.api.getProductStock({ productId: params.productId }),
context.api.getProduct({ id: params.productId }),
]);
// Return unified response
return { ...product, stock };
}
Using federation methods in the frontend
To call federation methods from your storefront, follow the extension methods guide.
TypeScript Support
Proper typing is essential when working with getApiClient to maintain type safety and get full autocomplete support in your IDE.
Quick Start: Direct Typing
Each integration exports an Endpoints type that you can pass to getApiClient:
import { type Endpoints as ContentfulEndpoints } from '@vsf-enterprise/contentful-api';
const contentful = await context.getApiClient<ContentfulEndpoints>("contentful");
For integrations with extensions, combine types based on the isNamespaced configuration:
import type { Endpoints, Endpoints as UnifiedEndpoints } from '@vsf-enterprise/sapcc-api';
// Namespaced: methods accessed via commerce.api.unified.method()
const commerce = await context.getApiClient<Endpoints & { unified: UnifiedEndpoints }>("commerce");
// Non-namespaced: methods accessed via commerce.api.method()
const commerce = await context.getApiClient<Endpoints & CustomEndpoints>("commerce");
Recommended: Centralized Type Helper
For better maintainability and to avoid repeating types across your codebase, create a centralized type-safe helper:
import type { ApiClient } from '@alokai/connect/middleware';
import type { SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';
import type { ContentfulIntegrationContext } from '@vsf-enterprise/contentful-api';
import type { Endpoints as UnifiedEndpoints } from '@vsf-enterprise/unified-api-sapcc';
import type { IntegrationContext } from '@/types';
interface IntegrationContextMap {
commerce: {
context: SapccIntegrationContext;
extensions: { unified: UnifiedEndpoints };
};
contentful: {
context: ContentfulIntegrationContext;
extensions: {};
};
}
export async function getTypedApiClient<TKey extends keyof IntegrationContextMap>(
context: IntegrationContext,
key: TKey,
): Promise<
ApiClient<
IntegrationContextMap[TKey]['context']['api'] & IntegrationContextMap[TKey]['extensions'],
IntegrationContextMap[TKey]['context']['config'],
IntegrationContextMap[TKey]['context']['client']
>
> {
return context.getApiClient(key);
}
Benefits:
- Define types once, use everywhere
- Full autocomplete and type checking
- Easy to maintain when adding new integrations or extensions
Usage:
import { getTypedApiClient } from '@/integrations/typedApiClient';
const contentful = await getTypedApiClient(context, "contentful");
const commerce = await getTypedApiClient(context, "commerce");
await commerce.api.unified.getProductDetails({ id: "123" });
Adding New Integrations
When you add a new integration to your project, update the IntegrationContextMap to include it.
Example: Adding a Legacy System Integration
Let's say you need to connect to a legacy inventory system:
1
Configure the integration in your middleware.config.ts:
export const config = {
integrations: {
commerce: commerceConfig,
contentful: contentfulConfig,
legacyInventory: legacyInventoryConfig,
},
};
2
Add the new integration to your IntegrationContextMap:
import type { LegacyInventoryContext } from './integrations/legacy-inventory/types';
interface IntegrationContextMap {
commerce: {
context: SapccIntegrationContext;
extensions: { unified: UnifiedEndpoints };
};
contentful: {
context: ContentfulIntegrationContext;
extensions: {};
};
legacyInventory: {
context: LegacyInventoryContext;
extensions: {}; // No extensions
};
}
3
Use it in your custom methods:
const inventory = await getTypedApiClient(context, "legacyInventory");
const stock = await inventory.api.getStock({ sku: "12345" });
Integration without Extensions
Most integrations start without extensions. Use extensions: {} for the type definition, and you can add extensions later as needed.
Adding Extensions to the Helper
When you add a new extension to an integration, you need to update the IntegrationContextMap types. The key difference is how you type the extensions property based on the isNamespaced configuration.
Example: Adding a Loyalty Extension
Let's say you're adding a loyalty program extension to your commerce integration:
// Extension configuration
const loyaltyExtension = {
name: 'loyalty',
extendApiMethods: {
getPoints: () => { /* ... */ }
},
isNamespaced: true,
}
How to Type in IntegrationContextMap:
Methods accessed via commerce.api.loyalty.method()
import { Endpoints as LoyaltyEndpoints } from './extensions/loyalty';
interface IntegrationContextMap {
commerce: {
context: SapccIntegrationContext;
extensions: {
unified: UnifiedEndpoints;
loyalty: LoyaltyEndpoints; // Nested property
};
};
}
Usage:
const commerce = await getTypedApiClient(context, "commerce");
await commerce.api.loyalty.getPoints({ customerId: "123" });
Key Takeaways:
- Namespaced (
isNamespaced: true): Add extension as a nested property{ extensionName: Endpoints } - Non-namespaced (
isNamespaced: false): Merge with existing extensions using& - Multiple namespaced: Add all as nested properties
{ ext1: Type1; ext2: Type2 } - Mixed: Combine both patterns
{ namespaced: Type } & NonNamespacedType
Learn More
See the Extensions Guide for details on creating and configuring extensions.