Skip to content
create-bff-layer logo

Skill: Create BFF Layer

create-bff-layer

Step-by-step guide for creating Backend for Frontend layers that aggregate multiple bounded contexts.

SKILL.md

Full skill instructions

Skill: Create BFF Layer

This skill teaches you how to create a Backend for Frontend (BFF) layer following architectural patterns. A BFF is a client-specific adapter that aggregates data from multiple bounded contexts and shapes responses for specific client surfaces.

A properly designed BFF enables clean separation between domain APIs and client needs. It aggregates calls, transforms responses, and handles client-specific concerns. The BFF remains stateless and never contains domain logic - it lives in Presentation and Adapter layers only.

Prerequisites

  • Understanding of Clean Architecture and DDD concepts
  • Multiple bounded contexts already implemented with public APIs
  • Clear understanding of what the client surface needs

Overview

You will: define client requirements, create bounded context clients, implement BFF aggregation, add response shaping, implement caching, create error handling, and test independently.

Step 1: Define Client Requirements

Create an OpenAPI contract that specifies the aggregated endpoints.

# contracts/​openapi.yaml
openapi: 3.0.3
info:
  title: Mobile Home BFF API
  version: 1.0.0
paths:
  /​home/​summary:
    get:
      summary: Get home energy summary
      operationId: getHomeSummary
      responses:
        '200':
          content:
            application/​json:
              schema:
                $ref: '#/​components/​schemas/​HomeSummaryResponse'
components:
  schemas:
    HomeSummaryResponse:
      type: object
      properties:
        facility_name:
          type: string
        total_assets:
          type: integer
        current_consumption_kw:
          type: number

The contract defines client-specific shapes that aggregate data from multiple bounded contexts.

Step 2: Create Backend Service Clients

Create client interfaces for each bounded context your BFF will call.

// clients/​interfaces

// FacilityClient communicates with the Facility bounded context.
INTERFACE FacilityClient
    METHOD GetUserFacility(ctx: Context, userID: String) RETURNS Result<Facility, Error>
END INTERFACE

// AssetClient communicates with the Asset bounded context.
INTERFACE AssetClient
    METHOD GetFacilityAssets(ctx: Context, facilityID: String) RETURNS Result<List<Asset>, Error>
END INTERFACE

// IntelligenceClient communicates with the Intelligence bounded context.
INTERFACE IntelligenceClient
    METHOD GetCurrentSchedule(ctx: Context, facilityID: String) RETURNS Result<Schedule, Error>
END INTERFACE

TYPE Facility
    id: String
    name: String
    address: String
END TYPE

TYPE Asset
    id: String
    name: String
    state: String
    currentLoadKW: Float
END TYPE

TYPE Schedule
    nextActionTime: String
    nextActionDesc: String
    expectedSavingsKWh: Float
END TYPE
// clients/​facility_client

TYPE HTTPFacilityClient
    baseURL: String
    httpClient: HttpClient
END TYPE

CONSTRUCTOR NewHTTPFacilityClient(baseURL: String, client: HttpClient) RETURNS HTTPFacilityClient
    RETURN HTTPFacilityClient{baseURL: baseURL, httpClient: client}
END CONSTRUCTOR

METHOD HTTPFacilityClient.GetUserFacility(ctx: Context, userID: String) RETURNS Result<Facility, Error>
    url = this.baseURL + "/​users/" + userID + "/​facility"
    request = NewHttpRequest(ctx, "GET", url)

    response = this.httpClient.Do(request)
    IF response.IsError() THEN
        RETURN Error("failed to call facility service: " + response.Error())
    END IF
    DEFER response.Body.Close()

    IF response.StatusCode == 404 THEN
        RETURN Error(ErrFacilityNotFound)
    END IF

    facility = Deserialize<Facility>(response.Body)
    RETURN Ok(facility.Value())
END METHOD

Each client calls a single bounded context's public API. The BFF never accesses databases directly.

Step 3: Implement BFF Handler with Aggregation

Create the BFF handler that orchestrates calls to multiple bounded contexts.

// handlers/​dto

// HomeSummaryResponse is the client-specific response shape.
TYPE HomeSummaryResponse
    facilityName: String
    totalAssets: Integer
    assetsOnline: Integer
    currentConsumptionKW: Float
    nextOptimization: OptimizationPreview  // Optional
    lastUpdated: DateTime
END TYPE

TYPE OptimizationPreview
    scheduledAt: DateTime
    action: String
    expectedSavingsKWh: Float
END TYPE
// handlers/​home_bff

// HomeBFFHandler aggregates data from multiple bounded contexts.
// It contains NO domain logic - only orchestration and response shaping.
TYPE HomeBFFHandler
    facilityClient: FacilityClient
    assetClient: AssetClient
    intelligenceClient: IntelligenceClient
END TYPE

CONSTRUCTOR NewHomeBFFHandler(
    f: FacilityClient,
    a: AssetClient,
    i: IntelligenceClient
) RETURNS HomeBFFHandler
    RETURN HomeBFFHandler{
        facilityClient: f,
        assetClient: a,
        intelligenceClient: i
    }
END CONSTRUCTOR

// GetHomeSummary aggregates data - pure orchestration, no business rules.
METHOD HomeBFFHandler.GetHomeSummary(ctx: Context, userID: String) RETURNS Result<HomeSummaryResponse, Error>
    // Call Facility bounded context
    facility = this.facilityClient.GetUserFacility(ctx, userID)
    IF facility.IsError() THEN
        RETURN Error("failed to get facility: " + facility.Error())
    END IF

    // Call Asset bounded context
    assets = this.assetClient.GetFacilityAssets(ctx, facility.Value().id)
    IF assets.IsError() THEN
        RETURN Error("failed to get assets: " + assets.Error())
    END IF

    // Call Intelligence bounded context (optional - don't fail if unavailable)
    schedule = this.intelligenceClient.GetCurrentSchedule(ctx, facility.Value().id)
    // Ignore error for optional data

    // Aggregate and shape response - NO business logic here
    RETURN Ok(this.shapeResponse(facility.Value(), assets.Value(), schedule.ValueOrNil()))
END METHOD

METHOD HomeBFFHandler.shapeResponse(
    facility: Facility,
    assets: List<Asset>,
    schedule: Schedule
) RETURNS HomeSummaryResponse
    assetsOnline = 0
    totalConsumption = 0.0

    FOR EACH asset IN assets DO
        IF asset.state == "online" THEN
            assetsOnline = assetsOnline + 1
        END IF
        totalConsumption = totalConsumption + asset.currentLoadKW
    END FOR

    response = HomeSummaryResponse{
        facilityName: facility.name,
        totalAssets: assets.Length(),
        assetsOnline: assetsOnline,
        currentConsumptionKW: totalConsumption,
        lastUpdated: DateTime.Now().ToUTC()
    }

    IF schedule != NULL THEN
        scheduledAt = DateTime.Parse(schedule.nextActionTime)
        response.nextOptimization = OptimizationPreview{
            scheduledAt: scheduledAt,
            action: schedule.nextActionDesc,
            expectedSavingsKWh: schedule.expectedSavingsKWh
        }
    END IF

    RETURN response
END METHOD

The handler only orchestrates and shapes. No if asset.Load > threshold style decisions.

Step 4: Add Optional Caching

For performance, add caching. This is technical caching, not domain state storage.

// cache/​cache

TYPE Cache
    client: CacheClient
    ttl: Duration
END TYPE

CONSTRUCTOR NewCache(client: CacheClient, ttl: Duration) RETURNS Cache
    RETURN Cache{client: client, ttl: ttl}
END CONSTRUCTOR

METHOD Cache.Get(ctx: Context, key: String, dest: Any) RETURNS Result<Any, Error>
    val = this.client.Get(ctx, key)
    IF val.IsError() THEN
        RETURN val.Error()
    END IF
    RETURN Deserialize(val.Value(), dest)
END METHOD

METHOD Cache.Set(ctx: Context, key: String, value: Any) RETURNS Result<Void, Error>
    data = Serialize(value)
    RETURN this.client.Set(ctx, key, data, this.ttl)
END METHOD

FUNCTION HomeSummaryKey(userID: String) RETURNS String
    RETURN "bff:home:summary:" + userID
END FUNCTION

Step 5: Implement Error Handling

Translate domain errors to client-friendly responses.

// clients/​errors

CONSTANT ErrFacilityNotFound = "facility not found"
CONSTANT ErrServiceUnavailable = "service unavailable"
CONSTANT ErrUnauthorized = "unauthorized"
// handlers/​errors

TYPE ErrorResponse
    error: String
    code: String
    message: String
END TYPE

FUNCTION MapErrorToHTTPStatus(err: Error) RETURNS Integer
    SWITCH err
        CASE ErrFacilityNotFound:
            RETURN 404
        CASE ErrUnauthorized:
            RETURN 401
        DEFAULT:
            RETURN 500
    END SWITCH
END FUNCTION

FUNCTION MapErrorToResponse(err: Error) RETURNS ErrorResponse
    SWITCH err
        CASE ErrFacilityNotFound:
            RETURN ErrorResponse{
                error: "not_found",
                code: "FACILITY_NOT_FOUND",
                message: "No facility associated with your account"
            }
        DEFAULT:
            RETURN ErrorResponse{
                error: "internal_error",
                code: "INTERNAL_ERROR",
                message: "Something went wrong"
            }
    END SWITCH
END FUNCTION

Step 6: Create Entry Point

Set up dependency injection in the handler.

// cmd/​api/​main

GLOBAL bffHandler: HomeBFFHandler

FUNCTION init()
    httpClient = NewHttpClient(timeout: 10 seconds)

    bffHandler = NewHomeBFFHandler(
        NewHTTPFacilityClient(GetEnv("FACILITY_SERVICE_URL"), httpClient),
        NewHTTPAssetClient(GetEnv("ASSET_SERVICE_URL"), httpClient),
        NewHTTPIntelligenceClient(GetEnv("INTELLIGENCE_SERVICE_URL"), httpClient)
    )
END FUNCTION

FUNCTION main()
    StartServer(handle)
END FUNCTION

FUNCTION handle(ctx: Context, request: HttpRequest) RETURNS HttpResponse
    userID = request.Context.Authorizer["sub"]
    IF userID == NULL THEN
        RETURN HttpResponse{StatusCode: 401}
    END IF

    response = bffHandler.GetHomeSummary(ctx, userID)
    IF response.IsError() THEN
        RETURN HttpResponse{StatusCode: MapErrorToHTTPStatus(response.Error())}
    END IF

    RETURN JsonResponse(200, response.Value())
END FUNCTION

Step 7: Test BFF Independently

Create tests that mock the bounded context clients.

// handlers/​home_bff_test

TYPE MockFacilityClient
    mock: Mock
END TYPE

METHOD MockFacilityClient.GetUserFacility(ctx: Context, userID: String) RETURNS Result<Facility, Error>
    args = this.mock.Called(ctx, userID)
    IF args.Get(0) == NULL THEN
        RETURN Error(args.Error(1))
    END IF
    RETURN Ok(args.Get(0) AS Facility)
END METHOD

TYPE MockAssetClient
    mock: Mock
END TYPE

METHOD MockAssetClient.GetFacilityAssets(ctx: Context, facilityID: String) RETURNS Result<List<Asset>, Error>
    args = this.mock.Called(ctx, facilityID)
    IF args.Get(0) == NULL THEN
        RETURN Error(args.Error(1))
    END IF
    RETURN Ok(args.Get(0) AS List<Asset>)
END METHOD

TYPE MockIntelligenceClient
    mock: Mock
END TYPE

METHOD MockIntelligenceClient.GetCurrentSchedule(ctx: Context, facilityID: String) RETURNS Result<Schedule, Error>
    args = this.mock.Called(ctx, facilityID)
    IF args.Get(0) == NULL THEN
        RETURN Error(args.Error(1))
    END IF
    RETURN Ok(args.Get(0) AS Schedule)
END METHOD


TEST GetHomeSummary_Success
    ctx = NewContext()
    facilityMock = NEW MockFacilityClient()
    assetMock = NEW MockAssetClient()
    intelligenceMock = NEW MockIntelligenceClient()

    facilityMock.mock.On("GetUserFacility", ctx, "user-123").Return(
        Facility{id: "f-1", name: "Home"},
        NULL
    )
    assetMock.mock.On("GetFacilityAssets", ctx, "f-1").Return(
        [
            Asset{id: "a-1", state: "online", currentLoadKW: 5.0},
            Asset{id: "a-2", state: "online", currentLoadKW: 7.5}
        ],
        NULL
    )
    intelligenceMock.mock.On("GetCurrentSchedule", ctx, "f-1").Return(
        Schedule{nextActionDesc: "Charge"},
        NULL
    )

    handler = NewHomeBFFHandler(facilityMock, assetMock, intelligenceMock)
    response = handler.GetHomeSummary(ctx, "user-123")

    ASSERT response.IsOk()
    ASSERT response.Value().facilityName == "Home"
    ASSERT response.Value().totalAssets == 2
    ASSERT response.Value().currentConsumptionKW == 12.5
END TEST


TEST GetHomeSummary_FacilityNotFound
    ctx = NewContext()
    facilityMock = NEW MockFacilityClient()
    assetMock = NEW MockAssetClient()
    intelligenceMock = NEW MockIntelligenceClient()

    facilityMock.mock.On("GetUserFacility", ctx, "unknown").Return(
        NULL,
        Error(ErrFacilityNotFound)
    )

    handler = NewHomeBFFHandler(facilityMock, assetMock, intelligenceMock)
    response = handler.GetHomeSummary(ctx, "unknown")

    ASSERT response.IsError()
    ASSERT response.Error().Contains(ErrFacilityNotFound)
END TEST

Verification Checklist

After creating your BFF layer, verify:

  • BFF has no domain logic (no business rules, no invariant checks)
  • BFF only calls public bounded context APIs
  • BFF does not access any database directly
  • BFF is stateless (caching is for performance only)
  • BFF does not emit domain events
  • BFF does not own domain models (uses view DTOs only)
  • Response shapes are client-specific, not domain shapes
  • Error handling translates domain errors to client-friendly responses
  • Each bounded context client is independently testable
  • BFF handler can be tested with mocked clients
  • OpenAPI contract matches implementation
  • Caching has appropriate TTL and invalidation strategy

More Other skills

Delegate independent tasks to specialized agents working concurrently with isolated context.

295.4K 206.3K
View

Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.

179.7K 100.1K
View

Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.

179.7K 89.5K
View

A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).

179.7K 76.1K
View

Fundamental design principles for writing maintainable, testable Python code.

40.2K 21.9K
View

Modern responsive layouts using container queries, fluid typography, CSS Grid, and mobile-first strategies.

40.2K 20K
View

Generate Playwright tests from scenarios using interactive browser exploration and validation.

39.7K 18.2K
View

Modern React state management with Redux Toolkit, Zustand, Jotai, and React Query for every state category.

40.2K 14.7K
View

GDPR-compliant data handling with consent management, data subject rights, and privacy controls.

40.2K 14.3K
View

Python error handling patterns including input validation, exception hierarchies, and partial failure handling. Use when implementing validation logic, designing exception strategies, handling batch processing failures, or building robust APIs.

40.2K 13.5K
View

Detailed multi-file refactor planning with safety checks and rollback guidance.

39.7K 13K
View