SHOPin Logo
Skip to main documentation content

Add a new integration

Add a new data source in the SHOPin storefront accelerator by creating a package under integrations/, implementing a service provider and NestJS services that satisfy @core/contracts, then registering the module in the BFF DataSourceFactory.

Use integrations/commercetools-api or integrations/mock-api as blueprints. The walkthrough uses the placeholder name example-api—rename types, tokens, and env vars to match your provider. Steps and code are illustrative; align file names and peer dependency versions with a live integration in the repo. Add the package under integrations/ so it is included by the root package.json workspaces.

1. Create the package directory

Bash
mkdir integrations/example-api
cd integrations/example-api

2. Package metadata

Add package.json, tsconfig.json, and any SDK dependencies your API needs. Mirror scripts and peerDependencies from an existing integration rather than copying version numbers from this page.

package.json (shape only):

JSON
{
  "name": "@integrations/example-api",
  "version": "0.0.0",
  "private": true,
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  },
  "scripts": {
    "build": "tsc --build",
    "dev": "tsc --build --watch",
    "lint": "eslint --fix ./src --max-warnings=0",
    "format": "prettier --write ./src",
    "check-types": "tsc --noEmit"
  },
  "dependencies": {
    "@core/contracts": "*",
    "@core/i18n": "*",
    "@config/constants": "*"
  },
  "devDependencies": {
    "@core/eslint-config": "*",
    "@core/prettier-config": "*",
    "@core/typescript-config": "*"
  },
  "peerDependencies": {
    "@nestjs/common": "^11.1.3",
    "@nestjs/config": "^4.0.2"
  }
}

tsconfig.json

JSON
{
  "extends": "@core/typescript-config/nestjs.json",
  "compilerOptions": {
    "outDir": "dist",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  },
  "include": ["src"],
  "exclude": ["dist", "node_modules"]
}

3. Service provider

The BFF DataSourceFactory calls getServices() on a registered provider token. Each integration supplies:

  • interfaces.ts — exports the provider type as BaseServiceProvider<ReturnType<…Impl['getServices']>> (data-source-interfaces.ts). That ties the return shape to Partial<AllServices> (same keys the BFF expects across data sources). The file **import type**s the implementation class only.
  • <integration-name>-service-provider.ts — Nest @Injectable() class that implements that type, injects domain services from src/services/, and getServices() returns an object whose keys are service names and values are those instances.

Reference: mock-api, commercetools-api. An auth provider package such as commercetools-auth uses BaseAuthServiceProvider and getAuthServices() instead.

TypeScript
// src/interfaces.ts
import type { BaseServiceProvider } from '@core/contracts/core/data-source-interfaces'
import type { ExampleServiceProviderImpl } from './example-service-provider'

export type ExampleServiceProvider = BaseServiceProvider<
  ReturnType<ExampleServiceProviderImpl['getServices']>
>
TypeScript
// src/example-service-provider.ts
import { Injectable } from '@nestjs/common'
import type { ExampleServiceProvider } from './interfaces'
import { ProductService } from './services/product.service'
import { NavigationService } from './services/navigation.service'

@Injectable()
export class ExampleServiceProviderImpl implements ExampleServiceProvider {
  constructor(
    private readonly productService: ProductService,
    private readonly navigationService: NavigationService
  ) {}

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

Domain classes in src/services/ implement @core/contracts (step 5); this class only composes them.

4. API client module

src/client/client.module.ts — Wire ConfigModule / ConfigService, expose a client token (e.g. EXAMPLE_CLIENT), return your SDK handle from useFactory.

TypeScript
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'

export const EXAMPLE_CLIENT = 'EXAMPLE_CLIENT'

@Module({
  imports: [ConfigModule],
  providers: [
    {
      provide: EXAMPLE_CLIENT,
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        const token = configService.getOrThrow<string>('EXAMPLE_TOKEN')
        const url = configService.getOrThrow<string>('EXAMPLE_URL')
        return { url, token }
      },
    },
  ],
  exports: [EXAMPLE_CLIENT],
})
export class ExampleClientModule {}

Document new variable names in the package README when you contribute the integration upstream.

5. Domain services

Implement each contract interface under src/services/, export from src/services/index.ts.

TypeScript
import { Injectable } from '@nestjs/common'
import type { ProductService as ProductServiceContract } from '@core/contracts/core/data-source-interfaces'
import type { ProductResponse } from '@core/contracts/product/product'

@Injectable()
export class ProductService implements ProductServiceContract {
  async getProduct(
    productKey: string,
    variantId?: string
  ): Promise<ProductResponse> {
    // Implement API calls here; map data to contract format and return
    throw new Error('Not implemented')
  }
}

6. Main NestJS module

Name the file <integration-name>-api.module.ts (e.g. example-api.module.ts). Existing integrations typically use @Global().

TypeScript
import { Global, Module } from '@nestjs/common'
import { ExampleServiceProviderImpl } from './example-service-provider'
import { ProductService } from './services/product.service'
import { NavigationService } from './services/navigation.service'
import { ExampleClientModule } from './client/client.module'

@Global()
@Module({
  imports: [ExampleClientModule],
  providers: [
    ProductService,
    NavigationService,
    {
      provide: 'EXAMPLE_SERVICE_PROVIDER',
      useClass: ExampleServiceProviderImpl,
    },
  ],
  exports: ['EXAMPLE_SERVICE_PROVIDER'],
})
export class ExampleApiModule {}

7. Package exports

src/index.ts

TypeScript
export * from './example-api.module'
export * from './client/client.module'
export * from './services'
export * from './interfaces'

8. Data source key (optional)

To select the integration via config, extend config/constants/src/data-source/index.ts (types, ALLOWED_DATA_SOURCES, default)—illustrative names:

TypeScript
export type DataSource = 'mock-set' | 'commercetools-set' | 'example-set'

export const ALLOWED_DATA_SOURCES: readonly DataSource[] = [
  'mock-set',
  'commercetools-set',
  'example-set',
] as const

export const DEFAULT_DATA_SOURCE: DataSource = 'mock-set'

Use the same string in the BFF switch and in the runtime configuration that sets DATA_SOURCE (or your project’s equivalent).

9. BFF DataSourceFactory

In apps/bff/src/data-source/data-source.factory.ts, inject the new provider token and return its getServices() from the appropriate branch (dedicated case, mixed composition, or single provider). See Data sources.

TypeScript
import type { ExampleServiceProvider } from '@integrations/example-api'

constructor(
  @Inject('EXAMPLE_SERVICE_PROVIDER')
  private readonly exampleServiceProvider: ExampleServiceProvider,
) {}

getServices() {
  switch (this.dataSource) {
    case 'commercetools-set':
      return this.commercetoolsServiceProvider.getServices()
    case 'mock-set':
      return this.mockServiceProvider.getServices()
    case 'example-set':
      return this.exampleServiceProvider.getServices()
    default:
      throw new Error(
        `Unknown data source: ${this.dataSource}. Allowed values: ${ALLOWED_DATA_SOURCES.join(', ')}`
      )
  }
}

10. BFF DataSourceModule

Import the new module in apps/bff/src/data-source/data-source.module.ts:

TypeScript
import { ExampleApiModule } from '@integrations/example-api'

@Module({
  imports: [
    MockApiModule,
    CommercetoolsApiModule,
    ExampleApiModule,
  ],
})

Wire @integrations/example-api (or your package name) into apps/bff/package.json dependencies if the monorepo does not already resolve it.

11. Package README and tooling

12. Verify

From the repository root, run npm run build (and your usual typecheck) so the new package and BFF compile. Exercise the new branch with DATA_SOURCE (or your equivalent) set to your new key.

Summary

StepOutcome
Package under integrations/NestJS module + provider token + services
ContractsTypes from @core/contracts / data-source interfaces
ConfigOptional new DataSource union + env
BFFFactory branch + DataSourceModule import + BFF dependency

Related

Back to Integrations · Back to How to work with SHOPin