Delegate independent tasks to specialized agents working concurrently with isolated context.
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
brand-guidelines
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.
theme-factory
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.
internal-comms
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.).
Fundamental design principles for writing maintainable, testable Python code.
responsive-design
Modern responsive layouts using container queries, fluid typography, CSS Grid, and mobile-first strategies.
Generate Playwright tests from scenarios using interactive browser exploration and validation.
Automated website exploration and test case generation using Playwright.
Modern React state management with Redux Toolkit, Zustand, Jotai, and React Query for every state category.
gdpr-data-handling
GDPR-compliant data handling with consent management, data subject rights, and privacy controls.
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.
refactor-plan
Detailed multi-file refactor planning with safety checks and rollback guidance.
