Vue Storefront is now Alokai! Learn More
Data Federation

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:

middleware.config.ts
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:

api/custom-methods/getPdp.ts
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

  1. Define Integration Keys: Ensure your integrations are properly configured with unique keys in middleware.config.ts.
  2. Create Federation Endpoint: Implement a new endpoint that will serve as the entry point for combined data requests.
  3. Use getApiClient: Within your endpoint, use getApiClient to access required integrations.
  4. 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:

api/custom-methods/getEnrichedProduct.ts
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:

integrations/sapcc/extensions/federation.ts
export const federationExtension = {
  name: "federation",
  extendApiMethods: {
    getEnrichedProduct,
  },
} satisfies ApiClientExtension
integrations/sapcc/config.ts
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 (usually Endpoints)
  • Config - Type for the integration configuration (usually MiddlewareConfig)
  • Client - Type for the HTTP client (usually AxiosInstance)

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:

integrations/sapcc/extensions/federation.ts
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:

  1. 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" });
  1. 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.