SHOPin Logo
Skip to main documentation content

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

  1. Create the contract (if it does not exist yet) — Zod schema and inferred types in the right domain file under core/contracts.
  2. Update the service interface — Add the method signature on the existing interface in data-source-interfaces.ts.
  3. Implement in integrations — Mock and Commercetools (and any other wired branch) each implement the method on their service class.
  4. Update the BFF feature service — Call dataSourceFactory.getServices().<service>.<method>().
  5. 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:

TypeScript
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):

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
@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

StepFocusOutcome
1Domain file under core/contracts/src/Zod + inferred types for the method
2data-source-interfaces.tsMethod on the service interface
3integrations/*Mock + Commercetools (and others) implement the method
4apps/bff/src/features/<domain>/Feature service → factory → integration
5Controller (optional)HTTP route + Schema.strip().parse

Related

Back to Integrations · Back to How to work with SHOPin