SHOPin Logo
Skip to main documentation content

Add a new service

Overview

In the SHOPin storefront accelerator, the Nest BFF talks to integration packages under integrations/. DataSourceFactory resolves DataSource per request (see 2. Multiple data sources on that page), picks one backend branch, and returns getServices()—one object of services (productService, cartService, …).

Adding a new service means: extend that object’s type (AllServices), implement the behaviour in the integration layer for each branch your factory actually uses (not necessarily every package in the monorepo), register each implementation on the matching service provider, then add a BFF feature that calls dataSourceFactory.getServices().yourService. The storefront uses the BFF only (Communication between presentation and BFF).

Call path: optional HTTP route → BFF feature service → getServices() → factory selects which integration’s getServices() built the map from DataSource → your method runs on that branch’s implementation.

This page walks through seven steps in order. Examples use Cart (cartService, GET /cart); names, paths, and schemas are illustrative.

Steps

  1. Describe the contract — Interface on AllServices in data-source-interfaces.ts; Zod types in @core/contracts.
  2. Implement in integrations — One class (or module) per wired data source that must satisfy the contract (e.g. mock, Commercetools).
  3. Wire the mock provider — Inject the service and return it from mock getServices(); export from services/index.ts.
  4. Wire other providers — Same pattern for Commercetools (and any other branch you added code for in step 2).
  5. Add the BFF feature*.service.ts (delegates to the factory), optional *.controller.ts, *.module.ts under apps/bff/src/features/<domain>/.
  6. Confirm the factory — Usually no edits: if step 1 and steps 3–4 are correct, DataSourceFactory already returns the new service for each DataSource key you support.
  7. Register the module — Import the new feature module in app.module.ts.

Step 1 — Contracts and data-source interface

Goal: Give every layer the same TypeScript and Zod shape for the new capability.

core/contracts/src/core/data-source-interfaces.ts — declare the service interface and add it to AllServices (the object shape DataSourceFactory ultimately returns):

TypeScript
export interface CartService {
  getCart(cartId: string): Promise<CartResponse>
}

export interface AllServices {
  // ...existing services
  cartService: CartService
}

core/contracts/src/cart/cart.ts — Zod schemas and inferred types for HTTP and integration boundaries (Maintain contracts):

TypeScript
import { z } from 'zod'

export const CartResponseSchema = z.object({
  id: z.string(),
})
export type CartResponse = z.infer<typeof CartResponseSchema>

export const CartIdSchema = z
  .string()
  .min(1, 'Cart ID is required')
  .max(255, 'Cart ID is too long')

Run npm run check-types from the repo root after editing data-source-interfaces.ts and contract files (see Maintain contracts and Add a new method).

Step 2 — Implement in integrations

Goal: Provide the real behaviour for each DataSource branch that must honour AllServices.

For each branch your data-source.factory.ts can return, the matching integration must expose the new service from getServices() whenever AllServices and your provider typings require that key for that branch. Skip integrations the factory never selects.

Mock (integrations/mock-api)

Add a Nest @Injectable() class under integrations/mock-api/src/services/. A minimal getCart is enough to prove types and factory wiring; the Cart example uses cart.service.ts.

TypeScript
import { Injectable } from '@nestjs/common'
import type { CartResponse } from '@core/contracts/cart/cart'

@Injectable()
export class CartService {
  async getCart(cartId: string): Promise<CartResponse> {
    return { id: cartId }
  }
}

The accelerator’s real cart.service.ts is larger: in-memory cartStore, Pino logging, NotFoundException for missing ids, CartResponseSchema.parse, plus createCart and line-item helpers—use that file when matching the shipped mock.

Commercetools (integrations/commercetools-api)

Same CartService surface as mock-api, but the implementation calls the Commercetools APIs (via the SDK or your wrapper) and maps the platform cart into @core/contracts. The block below is illustrative: it is not a copy of the repository; it only shows the usual shape (inject client → fetch by id → map → Zod parse). Your class names, client injection, and mapping helpers follow integrations/commercetools-api.

TypeScript
import { Injectable } from '@nestjs/common'
import { CartResponseSchema } from '@core/contracts/cart/cart'
import type { CartResponse } from '@core/contracts/cart/cart'

/**
 * Illustrative simplified Commercetools branch — replace `loadPlatformCart`
 * with real SDK calls and map fields into the contract shape.
 */
@Injectable()
export class CartService {
  async getCart(cartId: string): Promise<CartResponse> {
    const draft = await this.loadPlatformCart(cartId)
    return CartResponseSchema.parse(draft)
  }

  private async loadPlatformCart(cartId: string): Promise<{ id: string }> {
    // e.g. apiRoot.carts().withId({ ID: cartId }).get().execute(), then map to { id, … }
    return { id: cartId }
  }
}

Wire this service in step 4 the same way as the mock provider (constructor injection, getServices(), services/index.ts).

Step 3 — Mock service provider

Goal: Make Nest construct your new mock service class and include it in the object returned by mock getServices().

integrations/mock-api/src/mock-service-provider.ts — inject CartService and expose it from getServices() (constructor and return shape are illustrative):

TypeScript
@Injectable()
export class MockServiceProviderImpl implements MockServiceProvider {
  constructor(
    private readonly productService: ProductService,
    private readonly navigationService: NavigationService,
    private readonly cartService: CartService
  ) {}

  getServices() {
    return {
      productService: this.productService,
      navigationService: this.navigationService,
      cartService: this.cartService,
    }
  }
}

integrations/mock-api/src/services/index.ts:

TypeScript
export { CartService } from './cart.service'

If integrations/mock-api/src/mock-api.module.ts registers services via ...Object.values(services), exporting from services/index.ts is enough; otherwise add the class to providers / exports explicitly.

Step 4 — Commercetools (and other) service providers

Goal: Same wiring as step 3 for every other integration branch you implemented in step 2.

Repeat step 3 for each integration where you implemented step 2—for example integrations/commercetools-api: constructor injection, getServices() return shape, and services/index.ts. If the module lists providers and exports by hand, include the new service in both.

Step 5 — BFF feature module

Goal: Expose the capability through Nest: delegate to DataSourceFactory, validate HTTP input if you add a route.

Create apps/bff/src/features/<domain>/ with service, controller, and module. The Cart paths below are illustrative.

apps/bff/src/features/cart/cart.service.ts — delegates to DataSourceFactory:

TypeScript
import { Injectable } from '@nestjs/common'
import type { CartResponse } from '@core/contracts/cart/cart'
import { DataSourceFactory } from '../../data-source/data-source.factory'

@Injectable()
export class CartService {
  constructor(private readonly dataSourceFactory: DataSourceFactory) {}

  async getCart(cartId: string): Promise<CartResponse> {
    const { cartService } = this.dataSourceFactory.getServices()
    return cartService.getCart(cartId)
  }
}

apps/bff/src/features/cart/cart.controller.ts — validates query params with ZodQuery from apps/bff/src/common/validation:

TypeScript
import { Controller, Get } from '@nestjs/common'
import { CartService } from './cart.service'
import { ApiTags, ApiOkResponse, ApiQuery } from '@nestjs/swagger'
import { CartResponseSchema, CartIdSchema } from '@core/contracts/cart/cart'
import type { CartResponse } from '@core/contracts/cart/cart'
import { ZodQuery } from '../../common/validation'

@Controller('cart')
@ApiTags('cart')
export class CartController {
  constructor(private readonly cartService: CartService) {}

  @Get()
  @ApiOkResponse({ description: 'Cart data retrieved successfully' })
  @ApiQuery({ name: 'cartId', required: true, type: String, description: 'Cart ID' })
  async getCart(
    @ZodQuery(CartIdSchema, 'cartId') cartId: string
  ): Promise<CartResponse> {
    return CartResponseSchema.strip().parse(
      await this.cartService.getCart(cartId)
    )
  }
}

apps/bff/src/features/cart/cart.module.ts:

TypeScript
import { Module } from '@nestjs/common'
import { CartService } from './cart.service'
import { CartController } from './cart.controller'
import { DataSourceModule } from '../../data-source/data-source.module'

@Module({
  imports: [DataSourceModule],
  providers: [CartService],
  controllers: [CartController],
  exports: [CartService],
})
export class CartModule {}

Step 6 — DataSourceFactory

Goal: Ensure the active DataSource still maps to a provider whose getServices() includes your new key.

Once AllServices (step 1) and the providers for the factory branches you care about return the new service from getServices(), DataSourceFactory already exposes it—usually no structural factory changes beyond wiring you already use, plus the BFF feature layer. If you introduced a new DataSource value or merged providers, update the factory Map and module imports (Data sources).

Step 7 — Register the feature module

Goal: Load the new Nest module when the BFF starts.

apps/bff/src/app.module.ts — import the new feature module:

TypeScript
import { CartModule } from './features/cart/cart.module'

@Module({
  imports: [
    // ...existing modules
    CartModule,
  ],
})
export class AppModule {}

Summary

StepFocusOutcome
1data-source-interfaces.ts + core/contractsTyped AllServices + Zod for HTTP boundary
2integrations/*Implementation per wired branch
3–4Service providersNew key on each provider’s getServices()
5apps/bff/src/features/Nest service, optional controller, module
6data-source.factory.tsUsually verify only; edit Map if you add a data source
7app.module.tsFeature module in imports

Related

Back to Integrations · Back to How to work with SHOPin