SHOPin Logo
Skip to main documentation content

BFF

The Backend for Frontend in the SHOPin storefront accelerator is a NestJS app under apps/bff. It sits between the presentation layer and data sources, exposes HTTP routes the storefront calls through the BFF client, and keeps payloads aligned with @core/contracts. Shared configuration literals (not API DTOs) stay in Config & constants.

Runtime: NestJS — modules, controllers, providers, middleware.

Layout

Under apps/bff/src/:

  • app.module.ts — Root module; register feature modules in imports and global middleware here when needed.
  • common/ — Cross-cutting code: exception filters, language handling, Zod validation helpers, storage, tokens.
  • data-source/data-source.factory.ts and data-source.module.ts; wires the active integration set (e.g. mock, Commercetools). Details in Data sources.
  • features/ — Domain slices: *.module.ts, *.controller.ts, *.service.ts (plus guards or interceptors when used).
  • i18n/ — Locale payloads for the storefront; pairs with Translations and core/i18n.

Folder names beyond these are illustrative—inspect the repo for the current tree.

Module shape

PieceFileRole
Module[name].module.tsImports, providers, exports
HTTP[name].controller.tsRoutes, request/response handling
Service[name].service.tsOrchestration; calls DataSourceFactory (or other BFF services)

NestJS controllers · NestJS providers

Example: controller

Illustrative — navigation may differ in your checkout of the repo. Controllers delegate to services and parse responses with contract schemas:

TypeScript
import { Controller, Get } from '@nestjs/common'
import { NavigationService } from './navigation.service'
import { ApiTags, ApiOkResponse } from '@nestjs/swagger'
import { MainNavigationResponseSchema } from '@core/contracts/navigation/main-navigation'
import type { MainNavigationResponse } from '@core/contracts/navigation/main-navigation'

@Controller('navigation')
@ApiTags('navigation')
export class NavigationController {
  constructor(private readonly navigationService: NavigationService) {}

  @Get()
  @ApiOkResponse({
    description: 'Navigation data retrieved successfully',
  })
  async getNavigation(): Promise<MainNavigationResponse> {
    return MainNavigationResponseSchema.strip().parse(
      await this.navigationService.getNavigation()
    )
  }
}

Feature services

BFF services typically:

  • Delegate — Use DataSourceFactory.getServices() for integration-backed operations (product, cart, navigation, etc.).
  • Combine — Join several integration services in one handler when the contract aggregates multiple sources.
  • Stay consistent — Errors and logging follow Error handling and Logging.

Example: single integration service

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

  async getProductPage(
    productSlug: string,
    variantId?: string
  ): Promise<ProductPageResponse> {
    const { productService } = this.dataSourceFactory.getServices()
    const response = await productService.getProduct(productSlug, variantId)
    return {
      breadcrumb: response.breadcrumb,
      product: response.product,
    }
  }
}

Example: multiple services in one response

Illustrative — not every project exposes contentService / searchService; align with data-source.factory.ts and contracts.

TypeScript
async getPdpData(productKey: string, variantId?: string): Promise<ProductPageResponse> {
  const { productService, contentService, searchService } = this.dataSourceFactory.getServices()

  const [productData, teasers, relatedProducts] = await Promise.all([
    productService.getProduct(productKey, variantId),
    contentService.getTeasersForProduct(productKey),
    searchService.getRelatedProducts(productKey),
  ])

  return {
    ...productData,
    content: { teasers },
    recommendations: { relatedProducts },
  }
}

Checklist

TopicExpectation
Data accessUse DataSourceFactory for integration code paths — Data sources
ContractsRequest/response shapes from @core/contractsMaintain contracts
SwaggerDecorate routes as the codebase expects (tags, response metadata).
Input validationZod / pipes consistent with common/validationInput validation
ErrorsStable HTTP responses; no internal stack traces to clients — Error handling
LoggingStructured logs — Logging

DataSourceModule (see data-source.module.ts) registers integration modules and the factory so feature modules stay vendor-agnostic.

Related

Back to BFF & data · Back to How to work with SHOPin