Data Federation
Optimizing server requests through data federation is a common technique used within composable architectures that improves performance and reduces coupling between frotnend and backend API's. This guide shows how to use the getApiClient
method to retrieve and interact with integrations within the context of another integration to mix them together in one request.
Why?
- Minimized Network Traffic: Fewer server calls lead to reduced latency and enhanced responsiveness.
- Simplified Frontend Architecture: By grouping related server requests, the frontend logic becomes less complex and less coupled.
- Data Unification: You can retrieve data from multiple sources and unify it in one response under common data model unrelated to the details of underlying API's.
How?
Using getApiClient
Method to access different API client
The getApiClient
method allows you to access and interact with different integrations within the context of another integration. This is particularly useful when you need to combine data from multiple sources in a single request.
Type Safety
For information about type safety and available type parameters, see the Using Types with getApiClient section.
Basic Usage
To retrieve an integration client, use the getApiClient
method with the integration key:
const integrationClient = await context.getApiClient("integrationKey");
The integration key corresponds to the key defined in your middleware.config.ts
file. For example:
import { config as contentfulConfig } from './integrations/contentful';
import { config as commerceConfig } from './integrations/sapcc';
export const config = {
integrations: {
contentful: contentfulConfig, // integration key: "contentful"
commerce: commerceConfig, // integration key: "commerce"
},
}
Here's an example of using getApiClient
to combine product data from both commerce and CMS integrations:
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 },
): Promise<CustomMethodResponse> {
// Access the primary integration (SAPCC) directly
const sapccApi = context.api;
// Access the secondary integration (Contentful) using getApiClient
const contentful = await context.getApiClient<ContentfulEndpoints>("contentful");
// Fetch data from both integrations concurrently
const [product, content] = await Promise.all([
sapccApi.getProduct({ id: params.id }),
contentful.api.getEntries({
content_type: "product",
"fields.sku": params.id,
})
]);
return {
product,
content,
};
}
Implementation Steps
- Define Integration Keys: Ensure your integrations are properly configured with unique keys in
middleware.config.ts
. - Create Federation Endpoint: Implement a new endpoint that will serve as the entry point for combined data requests.
- Use getApiClient: Within your endpoint, use
getApiClient
to access required integrations. - Aggregate and Return: Combine the data from different sources and return a consolidated response.
Using federation methods in the frontend
To call the federation endpoint, you can follow the Using extension methods in the frontend guide.
Real-World Examples
The examples provided demonstrate practical uses of data federation:
Example 1: Fetching Custom Product Properties from Legacy Systems
This use case involves calling the commerce backend to fetch specific product data. Additionally, a separate call is made to a legacy custom system of the customer, to retrieve a custom product property (e.g., stock of the product). This data is used, for example, to display stock information on the product page.
First, let's create a custom method for enriching product data:
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 primary integration (SAPCC) directly
const sapccApi = context.api;
// Access the legacy system using getApiClient
const legacySystem = await context.getApiClient<LegacySystemEndpoints>("legacyCustomSystem");
// Fetch data from both systems concurrently
const [productStock, product] = await Promise.all([
legacySystem.api.getProductStock({
productId: params.productId,
}),
sapccApi.getProduct({
id: params.productId,
}),
]);
// Combine the data into a unified response
return {
...product,
stock: productStock,
};
}
Then, register the custom method in your integration:
export const federationExtension = {
name: "federation",
extendApiMethods: {
getEnrichedProduct,
},
} satisfies ApiClientExtension
export const config = {
// ... existing configuration
extensions: (extensions: ApiClientExtension[]) => [
...extensions,
unifiedApiExtension,
cdnExtension,
configSwitcherExtension,
+ federationExtension,
],
location: '@vsf-enterprise/sapcc-api/server',
} satisfies Integration<MiddlewareConfig>;
TypeScript Support
When working with getApiClient
, proper typing is crucial for maintaining type safety across your integrations. Each integration package exports its own types that you can use to type your API clients.
Available Types
Most integration packages export the following types:
import {
Endpoints, // Type for the API methods
MiddlewareConfig, // Type for the integration configuration
AxiosInstance, // Type for the HTTP client
} from "@vsf-enterprise/sapcc-api";
Using Types with getApiClient
The getApiClient
method accepts three generic type parameters:
getApiClient<Api, Config, Client>(key: string)
Where:
Api
- Type for the API methods (usuallyEndpoints
)Config
- Type for the integration configuration (usuallyMiddlewareConfig
)Client
- Type for the HTTP client (usuallyAxiosInstance
)
In most cases, you only need to specify the Api
type parameter:
// Most common usage - only specifying the API type
const client = await context.getApiClient<Endpoints>("integrationKey");
However, if you need to access API client config or client, you could pass all three parameters:
const client = await context.getApiClient<Endpoints, MiddlewareConfig, AxiosInstance>("integrationKey");
You can also use the integration context type to access these types:
import { SapccIntegrationContext } from "@vsf-enterprise/sapcc-api";
// The context type includes all necessary types
type Api = SapccIntegrationContext["api"];
type Config = SapccIntegrationContext["config"];
type Client = SapccIntegrationContext["client"];
Extensions
When using getApiClient
with integrations that have extensions, you can access both the base integration methods and the extension methods.
First, let's define our extension:
import type { SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';
import type { ApiClientExtension, WithoutContext } from '@alokai/connect/middleware';
export const federationExtension = {
name: "federation",
extendApiMethods: {
getEnrichedProduct: async (context: SapccIntegrationContext, params: { productId: string }) => {
// ... implementation
},
},
} satisfies ApiClientExtension;
// Define the types for the extension
export type FederationEndpoints = WithoutContext<(typeof federationExtension)['extendApiMethods']>;
You can access these methods in two ways:
- Namespaced Access - When the extension is configured with
isNamespaced: true
:
// Update the extension to use namespacing
export const federationExtension = {
name: "federation",
isNamespaced: true,
extendApiMethods: {
getEnrichedProduct: async (context: SapccIntegrationContext, params: { productId: string }) => {
// ... implementation
},
},
} satisfies ApiClientExtension;
// The extension methods are available under a namespace
const commerce = await context.getApiClient<Endpoints & {
federation: FederationEndpoints
}>("commerce");
const enrichedProduct = await commerce.api.federation.getEnrichedProduct({ productId: "123" });
- Direct Access - When the extension methods are merged with the base API:
// The extension methods are available directly on the API
const commerce = await context.getApiClient<Endpoints & FederationEndpoints>("commerce");
const enrichedProduct = await commerce.api.getEnrichedProduct({ productId: "123" });
Type-Safe Helper Functions
For better code organization and reusability, consider creating typed helper functions:
import { type Endpoints as ContentfulEndpoints } from '@vsf-enterprise/contentful-api';
import { type SapccIntegrationContext } from '@vsf-enterprise/sapcc-api';
// Helper for getting typed Contentful client
async function getContentfulApi(context: SapccIntegrationContext) {
const client = await context.getApiClient<ContentfulEndpoints>('contentful');
return client.api;
}
// Helper for getting typed Legacy System client
async function getLegacySystemApi(context: SapccIntegrationContext) {
const client = await context.getApiClient<LegacySystemEndpoints>('legacyCustomSystem');
return client.api;
}
These helper functions make your code more maintainable and provide better type inference throughout your application.