Add a new method
Overview
Add a new method on an existing domain service in the SHOPin storefront accelerator: define or extend Zod types in @core/contracts, add the method to the service interface in data-source-interfaces.ts, implement it on each integration branch your DataSourceFactory returns when that branch must satisfy the contract (see 2. Multiple data sources on that page), then delegate from the BFF feature service and optionally expose an HTTP route.
This page uses navigation and getFooterNavigation() as an example. API shapes, paths, and helper names are illustrative—mirror the real service names in your repo.
Steps
- Create the contract (if it does not exist yet) — Zod schema and inferred types in the right domain file under
core/contracts. - Update the service interface — Add the method signature on the existing interface in
data-source-interfaces.ts. - Implement in integrations — Mock and Commercetools (and any other wired branch) each implement the method on their service class.
- Update the BFF feature service — Call
dataSourceFactory.getServices().<service>.<method>(). - Expose an endpoint (optional) — Add a controller route and validate with the contract schema.
Step 1 — Create the contract (if missing)
Goal: Give the new method a shared response (and request, if any) type for integrations and the BFF.
In the appropriate domain file under core/contracts/src/ (this example uses a dedicated footer-navigation module). See Maintain contracts.
core/contracts/src/navigation/ — add a file such as footer-navigation.ts when the schema is new, or extend an existing module:
import { z } from 'zod'
import { LinkResponseSchema } from '../core/link'
export const FooterNavigationResponseSchema = z.object({
items: z.array(LinkResponseSchema),
})
export type FooterNavigationResponse = z.infer<typeof FooterNavigationResponseSchema>
Step 2 — Update the service interface
Goal: Put the method on the shared interface so every provider and the BFF agree on the signature.
core/contracts/src/core/data-source-interfaces.ts — add the method to the existing service interface (here, NavigationService):
import type { MainNavigationResponse } from '@core/contracts/navigation/main-navigation'
import type { FooterNavigationResponse } from '@core/contracts/navigation/footer-navigation'
export interface NavigationService {
getNavigation(): Promise<MainNavigationResponse>
getFooterNavigation(): Promise<FooterNavigationResponse>
}
Run npm run check-types from the repo root after editing contracts (see Maintain contracts and Add a new service).
Step 3 — Implement in integrations
Goal: Every DataSource branch that exposes navigationService must implement getFooterNavigation() with the same return type.
Skip branches your factory never maps to that service. When the integration class name matches the interface name (e.g. both NavigationService), import the interface as a type alias and implements it (shown in the mock block).
Mock (integrations/mock-api)
Helpers such as MockApi, generateSeed, and MOCK_API match the style of the mock package; adjust imports to your tree.
integrations/mock-api/src/services/navigation.service.ts:
import { Injectable, Inject } from '@nestjs/common'
import type { NavigationService as NavigationServiceContract } from '@core/contracts/core/data-source-interfaces'
import type { FooterNavigationResponse } from '@core/contracts/navigation/footer-navigation'
// MOCK_API, MockApi, generateSeed — from the mock package (see repo `navigation.service.ts`)
function createFooterNavigation(
faker: ReturnType<MockApi['getFaker']>
): FooterNavigationResponse {
return {
items: faker.helpers.multiple(
() => {
const text = faker.company.buzzNoun()
return {
text,
href: `/${faker.helpers.slugify(text).toLowerCase()}`,
}
},
{ count: 6 }
),
}
}
@Injectable()
export class NavigationService implements NavigationServiceContract {
constructor(@Inject(MOCK_API) private readonly mockApi: MockApi) {}
async getFooterNavigation(): Promise<FooterNavigationResponse> {
const faker = this.mockApi.getFaker()
faker.seed(generateSeed('footer'))
return createFooterNavigation(faker)
}
}
Commercetools (integrations/commercetools-api)
Illustrative shape only—wire the real Commercetools client and mappers from integrations/commercetools-api:
import { Injectable } from '@nestjs/common'
import {
FooterNavigationResponseSchema,
type FooterNavigationResponse,
} from '@core/contracts/navigation/footer-navigation'
import type { NavigationService as NavigationServiceContract } from '@core/contracts/core/data-source-interfaces'
/** Illustrative: replace with SDK calls + mapping into FooterNavigationResponse. */
@Injectable()
export class NavigationService implements NavigationServiceContract {
async getFooterNavigation(): Promise<FooterNavigationResponse> {
const draft = await this.loadFooterLinksFromPlatform()
return FooterNavigationResponseSchema.parse(draft)
}
private async loadFooterLinksFromPlatform(): Promise<{ items: unknown[] }> {
// Map storefront / menu API result → contract `items`
return { items: [] }
}
}
Step 4 — BFF feature service
Goal: The BFF does not call a vendor SDK directly; it uses DataSourceFactory.
apps/bff/src/features/navigation/navigation.service.ts — delegate to the active integration:
import { Injectable } from '@nestjs/common'
import type { FooterNavigationResponse } from '@core/contracts/navigation/footer-navigation'
import { DataSourceFactory } from '../../data-source/data-source.factory'
@Injectable()
export class NavigationService {
constructor(private readonly dataSourceFactory: DataSourceFactory) {}
async getFooterNavigation(): Promise<FooterNavigationResponse> {
const { navigationService } = this.dataSourceFactory.getServices()
return navigationService.getFooterNavigation()
}
}
Step 5 — Expose an endpoint (optional)
Goal: Let the presentation app call the method through HTTP with a validated response.
apps/bff/src/features/navigation/navigation.controller.ts — add a route next to existing handlers (e.g. getNavigation()). Excerpt:
@Get('footer')
@ApiOkResponse({
description: 'Footer navigation data retrieved successfully',
})
async getFooterNavigation(): Promise<FooterNavigationResponse> {
return FooterNavigationResponseSchema.strip().parse(
await this.navigationService.getFooterNavigation()
)
}
The presentation app uses the BFF client against this path once the route exists and contracts match.
Summary
| Step | Focus | Outcome |
|---|---|---|
| 1 | Domain file under core/contracts/src/ | Zod + inferred types for the method |
| 2 | data-source-interfaces.ts | Method on the service interface |
| 3 | integrations/* | Mock + Commercetools (and others) implement the method |
| 4 | apps/bff/src/features/<domain>/ | Feature service → factory → integration |
| 5 | Controller (optional) | HTTP route + Schema.strip().parse |