Data Mappers
Data mappers play a crucial role in Archibald's architecture by serving as the translation layer between external services and the application's user interface.
What are Mappers?
Mappers are classes responsible for transforming raw data, typically received from external APIs, into a structured format that aligns with your application's internal data models. They act as a crucial translation layer, ensuring consistency and decoupling your application logic from the specifics of third-party API responses.
Purpose of Mappers
- Data Standardization: Convert varied API response formats into a consistent, application-wide data structure.
- Decoupling: Isolate your application's core logic from external API changes. If an API changes, you only need to update the mapper, not every place the data is consumed.
- Customization: Extend or override default mapping logic to add custom fields, apply business rules, or filter data as needed by your project.
- Readability and Maintainability: Centralize data transformation logic, making it easier to understand and maintain how data is processed.
Declaring and Registering Mappers
Mappers are typically implemented as classes that often extend a base mapper provided by Archibald (e.g., CommerceProductMapper). They expose methods to map specific entities.
There are two primary ways to utilize mappers in Archibald:
- Registered with Modules: Some Archibald modules (like
ProductModuleandSearchModule) allow you to register custom mappers that will be applied automatically when fetching data through those modules. - Used Directly: You can also instantiate and use mappers directly within your services or providers (e.g., within your
CMSProvider's data transformation logic).
Code Examples
1. Declaring a Custom Mapper
Consider the ProjectCommerceProductMapper which extends Archibald's CommerceProductMapper. In this example, we override the base product mapping behavior to inject custom branding and stock-level status fields:
// templates/shop/src/shop/server/classes/project-commerce-product-mapper.ts
import { CommerceProductMapper } from '@archibald/commerce/product';
import type { CommerceProduct } from '@archibald/commerce/product';
import type { Product } from '@archibald/storefront';
export class ProjectCommerceProductMapper extends CommerceProductMapper {
public override mapProduct(source: CommerceProduct): Product {
// Call the parent mapper to get the base product transformation
const product = super.mapProduct(source);
// Add or override custom fields specific to your project
return {
...product,
custom: {
rating: source.averageRating,
summary: source.summary,
brand: source.manufacturer,
stockLevelStatus: source.stock?.stockLevelStatus ?? 'outOfStock',
// ... and other project-specific fields
}
};
}
}
2. Using a Mapper Directly (Manual Invocation)
When a response is not fetched through a module — a custom service calling the commerce API itself — hold the mapper as a field and invoke it on the raw payload. The shop template's OrderService does exactly this to normalise the products nested inside order entries:
// templates/shop/src/shop/server/api/services/hybris/order.ts
import { ProjectCommerceProductMapper } from 'shop/server/classes/project-commerce-product-mapper';
export class OrderService {
private readonly productMapper = new ProjectCommerceProductMapper();
public async getOrder(code: string) {
const response = await this.hybrisHttpService.get<HybrisOrder>(hybris.url, { headers: hybris.headers });
// Each entry carries a raw commerce product — map it to the standardized model,
// folding the entry's own price into the product first.
const mappedEntries = (response.body.entries ?? []).map((entry) => ({
...entry,
product: this.productMapper.mapProduct({ ...entry.product, price: entry.basePrice })
}));
return { ...response.body, entries: mappedEntries };
}
}
Every product is now a typed Product — including the custom fields added by the override — and is ready to render.
3. Registering a Mapper with a Module (Automatic Invocation)
In most cases you don't instantiate anything. You hand the mapper class to the provider when registering the module in your Server, and the provider applies it to every response it maps. The same mapper can be shared by several modules:
// templates/shop/src/shop/server/module/server.tsx
import { CommerceProductProvider } from '@archibald/commerce/product';
import { CommerceSearchProvider } from '@archibald/commerce/search';
import { CoreServer } from '@archibald/server';
import { ProjectCommerceProductMapper } from 'shop/server/classes/project-commerce-product-mapper';
// Modules are re-exported through the project barrel, which mixes framework modules
// (ProductModule, SearchModule from @archibald/product / @archibald/search) with project-local ones.
import { ProductModule, SearchModule } from 'shop/server/api/modules';
export class Server extends CoreServer {
public async initModules() {
await this.registerModules([
new SearchModule({
provider: new CommerceSearchProvider({ config: () => this.configService.get('hybris.api'), mappers: [ProjectCommerceProductMapper] })
}),
new ProductModule({
// Pass the class, not an instance — the provider constructs it.
provider: new CommerceProductProvider({ config: () => this.configService.get('hybris.api'), mappers: [ProjectCommerceProductMapper] })
})
// ... other modules
]);
}
}