In the second part of this multi-part series of articles, we present MACA, our open-source solution for AI-composable commerce.
With Radixia Maca, our open-source initiative for AI-driven composable commerce, we're not just rethinking e-commerce—we're re-architecting it. At the heart of this architecture is infrastructure-as-code, which ensures consistency, traceability, and repeatability across deployments. In this chapter, we shift from design principles to execution, implementing the complete serverless infrastructure for our platform using AWS CDK (Cloud Development Kit). This gives us the flexibility to define complex cloud architectures in code, leverage reusable constructs, and scale components independently.
In this section, we showcase how to implement the complete infrastructure for our serverless e-commerce using AWS CDK. This approach allows us to define infrastructure as code, making it easier to manage, version, and deploy.
CDK Project Structure
Our CDK project is organized into modular constructs, each responsible for a specific part of the infrastructure:
- Main Stack: Coordinates all constructs and defines dependencies
- API Gateway Construct: Manages REST APIs for the frontend
- Bedrock Search Construct: Configures semantic search with Bedrock
- Data Storage Construct: Manages DynamoDB and other storage services
- Frontend Construct: Configures frontend hosting
- MCP Server Construct: Implements the Model Context Protocol server
- Strapi Service Construct: Configures the Strapi service on Fargate
Let's look at the implementation of each component in detail.
Main Stack
The main stack (ServerlessEcommerceStack
) is the entry point of our CDK infrastructure. It coordinates all constructs and defines dependencies between them.
// lib/serverless-ecommerce-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { ApiGatewayConstruct } from './api-gateway-construct';
import { BedrockSearchConstruct } from './bedrock-search-construct';
import { DataStorageConstruct } from './data-storage-construct';
import { FrontendConstruct } from './frontend-construct';
import { McpServerConstruct } from './mcp-server-construct';
import { StrapiServiceConstruct } from './strapi-service-construct';
export class ServerlessEcommerceStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create the data storage construct
const dataStorage = new DataStorageConstruct(this, 'DataStorage');
// Create the MCP server construct
const mcpServer = new McpServerConstruct(this, 'McpServer', {
vpc: dataStorage.vpc,
productsTable: dataStorage.productsTable,
commerceLayerSecret: dataStorage.commerceLayerSecret,
strapiSecret: dataStorage.strapiSecret,
});
// Create the Bedrock search construct
const bedrockSearch = new BedrockSearchConstruct(this, 'BedrockSearch', {
productsTable: dataStorage.productsTable,
commerceLayerSecret: dataStorage.commerceLayerSecret,
strapiSecret: dataStorage.strapiSecret,
mcpServerUrl: mcpServer.serviceUrl,
});
// Create the Strapi service construct
const strapiService = new StrapiServiceConstruct(this, 'StrapiService', {
vpc: dataStorage.vpc,
strapiSecret: dataStorage.strapiSecret,
commerceLayerSecret: dataStorage.commerceLayerSecret,
syncLambda: dataStorage.strapiClSyncLambda,
});
// Create the API Gateway construct
const apiGateway = new ApiGatewayConstruct(this, 'ApiGateway', {
semanticSearchLambda: bedrockSearch.semanticSearchLambda,
orderProcessorLambda: dataStorage.orderProcessorLambda,
inventoryManagerLambda: dataStorage.inventoryManagerLambda,
});
// Create the frontend construct
const frontend = new FrontendConstruct(this, 'Frontend', {
apiEndpoint: apiGateway.apiEndpoint,
strapiEndpoint: strapiService.serviceUrl,
});
// Outputs
new cdk.CfnOutput(this, 'ApiEndpoint', {
value: apiGateway.apiEndpoint,
description: 'API Gateway endpoint',
});
new cdk.CfnOutput(this, 'FrontendUrl', {
value: frontend.distributionUrl,
description: 'Frontend URL',
});
new cdk.CfnOutput(this, 'StrapiServiceUrl', {
value: strapiService.serviceUrl,
description: 'Strapi service URL',
});
new cdk.CfnOutput(this, 'McpServerUrl', {
value: mcpServer.serviceUrl,
description: 'MCP server URL',
});
}
}
Data Storage Construct
The DataStorageConstruct
manages all data storage resources, including DynamoDB, Secrets Manager, and related Lambda functions.
// lib/data-storage-construct.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as path from 'path';
export interface DataStorageConstructProps {
// Optional properties
}
export class DataStorageConstruct extends Construct {
public readonly productsTable: dynamodb.Table;
public readonly ordersTable: dynamodb.Table;
public readonly inventoryTable: dynamodb.Table;
public readonly commerceLayerSecret: secretsmanager.Secret;
public readonly strapiSecret: secretsmanager.Secret;
public readonly vpc: ec2.Vpc;
public readonly strapiClSyncLambda: lambda.Function;
public readonly orderProcessorLambda: lambda.Function;
public readonly inventoryManagerLambda: lambda.Function;
public readonly lowStockTopic: sns.Topic;
constructor(scope: Construct, id: string, props?: DataStorageConstructProps) {
super(scope, id);
// Create a VPC for services
this.vpc = new ec2.Vpc(this, 'VPC', {
maxAzs: 2,
natGateways: 1,
});
// Create DynamoDB tables
this.productsTable = new dynamodb.Table(this, 'ProductsTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
});
// Add a global secondary index for category
this.productsTable.addGlobalSecondaryIndex({
indexName: 'CategoryIndex',
partitionKey: { name: 'category', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});
this.ordersTable = new dynamodb.Table(this, 'OrdersTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
});
this.inventoryTable = new dynamodb.Table(this, 'InventoryTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
});
// Create secrets for credentials
this.commerceLayerSecret = new secretsmanager.Secret(this, 'CommerceLayerSecret', {
secretName: 'commerceLayer/credentials',
description: 'Commerce Layer credentials',
generateSecretString: {
secretStringTemplate: JSON.stringify({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
endpoint: 'https://yourdomain.commercelayer.io',
}) ,
generateStringKey: 'password',
},
});
this.strapiSecret = new secretsmanager.Secret(this, 'StrapiSecret', {
secretName: 'strapi/credentials',
description: 'Strapi CMS credentials',
generateSecretString: {
secretStringTemplate: JSON.stringify({
apiKey: 'your-api-key',
endpoint: 'https://your-strapi-endpoint.com',
}) ,
generateStringKey: 'password',
},
});
// Create an SNS topic for low stock notifications
this.lowStockTopic = new sns.Topic(this, 'LowStockTopic', {
displayName: 'Low stock notifications',
});
// Create Lambda functions
this.strapiClSyncLambda = new lambda.Function(this, 'StrapiClSyncLambda', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/strapi-cl-sync')),
timeout: cdk.Duration.seconds(30),
memorySize: 256,
environment: {
COMMERCE_LAYER_SECRET_ARN: this.commerceLayerSecret.secretArn,
STRAPI_SECRET_ARN: this.strapiSecret.secretArn,
PRODUCTS_TABLE: this.productsTable.tableName,
},
});
this.orderProcessorLambda = new lambda.Function(this, 'OrderProcessorLambda', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/order-processor')),
timeout: cdk.Duration.seconds(30),
memorySize: 256,
environment: {
COMMERCE_LAYER_SECRET_ARN: this.commerceLayerSecret.secretArn,
ORDERS_TABLE: this.ordersTable.tableName,
FROM_EMAIL: 'noreply@serverlessday.com',
EMAIL_TEMPLATE_NAME: 'OrderConfirmation',
},
});
this.inventoryManagerLambda = new lambda.Function(this, 'InventoryManagerLambda', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/inventory-manager')),
timeout: cdk.Duration.seconds(30),
memorySize: 256,
environment: {
COMMERCE_LAYER_SECRET_ARN: this.commerceLayerSecret.secretArn,
INVENTORY_TABLE: this.inventoryTable.tableName,
LOW_STOCK_THRESHOLD: '5',
LOW_STOCK_TOPIC_ARN: this.lowStockTopic.topicArn,
},
});
// Grant necessary permissions
this.commerceLayerSecret.grantRead(this.strapiClSyncLambda);
this.strapiSecret.grantRead(this.strapiClSyncLambda);
this.productsTable.grantReadWriteData(this.strapiClSyncLambda);
this.commerceLayerSecret.grantRead(this.orderProcessorLambda);
this.ordersTable.grantReadWriteData(this.orderProcessorLambda);
this.commerceLayerSecret.grantRead(this.inventoryManagerLambda);
this.inventoryTable.grantReadWriteData(this.inventoryManagerLambda);
this.lowStockTopic.grantPublish(this.inventoryManagerLambda);
// Create an EventBridge rule to run the inventoryManagerLambda daily
new events.Rule(this, 'DailyInventorySync', {
schedule: events.Schedule.cron({ minute: '0', hour: '0' }),
targets: [new targets.LambdaFunction(this.inventoryManagerLambda)],
});
}
}
Bedrock Search Construct
The BedrockSearchConstruct
configures semantic search using Amazon Bedrock and the Model Context Protocol.
// lib/bedrock-search-construct.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as path from 'path';
export interface BedrockSearchConstructProps {
productsTable: dynamodb.Table;
commerceLayerSecret: secretsmanager.Secret;
strapiSecret: secretsmanager.Secret;
mcpServerUrl: string;
}
export class BedrockSearchConstruct extends Construct {
public readonly semanticSearchLambda: lambda.Function;
public readonly bedrockAgentId: string;
constructor(scope: Construct, id: string, props: BedrockSearchConstructProps) {
super(scope, id);
// Create an IAM role for Bedrock access
const bedrockRole = new iam.Role(this, 'BedrockRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
],
});
// Add policies for Bedrock access
bedrockRole.addToPolicy(
new iam.PolicyStatement({
actions: [
'bedrock:InvokeModel',
'bedrock:InvokeAgent',
'bedrock:CreateAgent',
'bedrock:UpdateAgent',
],
resources: ['*'], // Limit to specific resources in production
})
);
// Create the Lambda function for semantic search
this.semanticSearchLambda = new lambda.Function(this, 'SemanticSearchLambda', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda/semantic-search')),
timeout: cdk.Duration.seconds(30),
memorySize: 512,
environment: {
COMMERCE_LAYER_SECRET_ARN: props.commerceLayerSecret.secretArn,
STRAPI_SECRET_ARN: props.strapiSecret.secretArn,
PRODUCTS_TABLE: props.productsTable.tableName,
MCP_SERVER_URL: props.mcpServerUrl,
},
role: bedrockRole,
});
// Grant necessary permissions
props.commerceLayerSecret.grantRead(this.semanticSearchLambda);
props.strapiSecret.grantRead(this.semanticSearchLambda);
props.productsTable.grantReadData(this.semanticSearchLambda);
// Set the Bedrock agent ID (in a real implementation, this would be created via CDK)
this.bedrockAgentId = 'agent-id-placeholder';
// Outputs
new cdk.CfnOutput(this, 'SemanticSearchLambdaArn', {
value: this.semanticSearchLambda.functionArn,
description: 'ARN of the semantic search Lambda function',
});
}
}
MCP Server Construct
The McpServerConstruct
implements the Model Context Protocol server that acts as a connector between Bedrock Agents and data sources.
// lib/mcp-server-construct.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as path from 'path';
export interface McpServerConstructProps {
vpc: ec2.Vpc;
productsTable: dynamodb.Table;
commerceLayerSecret: secretsmanager.Secret;
strapiSecret: secretsmanager.Secret;
}
export class McpServerConstruct extends Construct {
public readonly serviceUrl: string;
constructor(scope: Construct, id: string, props: McpServerConstructProps) {
super(scope, id);
// Create an ECR repository for the Docker image
const repository = new ecr.Repository(this, 'McpServerRepository', {
repositoryName: 'mcp-server',
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
});
// Create an ECS cluster
const cluster = new ecs.Cluster(this, 'McpServerCluster', {
vpc: props.vpc,
});
// Create an execution role for the ECS task
const executionRole = new iam.Role(this, 'McpServerExecutionRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'),
],
});
// Create a task role for the ECS task
const taskRole = new iam.Role(this, 'McpServerTaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
// Grant necessary permissions
props.commerceLayerSecret.grantRead(taskRole);
props.strapiSecret.grantRead(taskRole);
props.productsTable.grantReadData(taskRole);
// Create a Fargate service
const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'McpServerService', {
cluster,
memoryLimitMiB: 1024,
cpu: 512,
desiredCount: 2,
taskImageOptions: {
image: ecs.ContainerImage.fromAsset(path.join(__dirname, '../docker/mcp-server')),
containerPort: 3000,
environment: {
COMMERCE_LAYER_SECRET_ARN: props.commerceLayerSecret.secretArn,
STRAPI_SECRET_ARN: props.strapiSecret.secretArn,
PRODUCTS_TABLE: props.productsTable.tableName,
NODE_ENV: 'production',
},
taskRole,
executionRole,
},
publicLoadBalancer: true,
});
// Configure health check
fargateService.targetGroup.configureHealthCheck({
path: '/health',
interval: cdk.Duration.seconds(60),
timeout: cdk.Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 5,
});
// Configure auto scaling
const scaling = fargateService.service.autoScaleTaskCount({
minCapacity: 2,
maxCapacity: 10,
});
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
scaleInCooldown: cdk.Duration.seconds(60),
scaleOutCooldown: cdk.Duration.seconds(60),
});
// Set the service URL
this.serviceUrl = `http://${fargateService.loadBalancer.loadBalancerDnsName}`;
// Outputs
new cdk.CfnOutput(this, 'McpServerServiceUrl', {
value: this.serviceUrl,
description: 'MCP Server service URL',
}) ;
}
}
Strapi Service Construct
The StrapiServiceConstruct
configures the Strapi CMS service on AWS Fargate.
// lib/strapi-service-construct.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as path from 'path';
export interface StrapiServiceConstructProps {
vpc: ec2.Vpc;
strapiSecret: secretsmanager.Secret;
commerceLayerSecret: secretsmanager.Secret;
syncLambda: lambda.Function;
}
export class StrapiServiceConstruct extends Construct {
public readonly serviceUrl: string;
constructor(scope: Construct, id: string, props: StrapiServiceConstructProps) {
super(scope, id);
// Create a security group for the database
const dbSecurityGroup = new ec2.SecurityGroup(this, 'DatabaseSecurityGroup', {
vpc: props.vpc,
description: 'Security group for Strapi database',
allowAllOutbound: true,
});
// Create a PostgreSQL database
const database = new rds.DatabaseInstance(this, 'StrapiDatabase', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_14,
}),
instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.SMALL),
vpc: props.vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
securityGroups: [dbSecurityGroup],
allocatedStorage: 20,
maxAllocatedStorage: 100,
databaseName: 'strapi',
credentials: rds.Credentials.fromGeneratedSecret('strapi'),
backupRetention: cdk.Duration.days(7),
deleteAutomatedBackups: true,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
});
// Create an ECR repository for the Docker image
const repository = new ecr.Repository(this, 'StrapiRepository', {
repositoryName: 'strapi-cms',
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
});
// Create an ECS cluster
const cluster = new ecs.Cluster(this, 'StrapiCluster', {
vpc: props.vpc,
});
// Create an execution role for the ECS task
const executionRole = new iam.Role(this, 'StrapiExecutionRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'),
],
});
// Create a task role for the ECS task
const taskRole = new iam.Role(this, 'StrapiTaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
// Grant necessary permissions
props.strapiSecret.grantRead(taskRole);
props.commerceLayerSecret.grantRead(taskRole);
database.secret?.grantRead(taskRole);
props.syncLambda.grantInvoke(taskRole);
// Create a Fargate service
const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'StrapiService', {
cluster,
memoryLimitMiB: 2048,
cpu: 1024,
desiredCount: 2,
taskImageOptions: {
image: ecs.ContainerImage.fromAsset(path.join(__dirname, '../docker/strapi-cms')),
containerPort: 1337,
environment: {
DATABASE_CLIENT: 'postgres',
DATABASE_HOST: database.dbInstanceEndpointAddress,
DATABASE_PORT: database.dbInstanceEndpointPort,
DATABASE_NAME: 'strapi',
NODE_ENV: 'production',
SYNC_LAMBDA_URL: props.syncLambda.functionUrl?.url || '',
COMMERCE_LAYER_CLIENT_ID: '${COMMERCE_LAYER_CLIENT_ID}',
COMMERCE_LAYER_CLIENT_SECRET: '${COMMERCE_LAYER_CLIENT_SECRET}',
COMMERCE_LAYER_ENDPOINT: '${COMMERCE_LAYER_ENDPOINT}',
},
secrets: {
DATABASE_USERNAME: ecs.Secret.fromSecretsManager(database.secret!, 'username'),
DATABASE_PASSWORD: ecs.Secret.fromSecretsManager(database.secret!, 'password'),
ADMIN_JWT_SECRET: ecs.Secret.fromSecretsManager(props.strapiSecret, 'adminJwtSecret'),
JWT_SECRET: ecs.Secret.fromSecretsManager(props.strapiSecret, 'jwtSecret'),
API_TOKEN_SALT: ecs.Secret.fromSecretsManager(props.strapiSecret, 'apiTokenSalt'),
},
taskRole,
executionRole,
},
publicLoadBalancer: true,
});
// Configure health check
fargateService.targetGroup.configureHealthCheck({
path: '/_health',
interval: cdk.Duration.seconds(60),
timeout: cdk.Duration.seconds(5),
healthyThresholdCount: 2,
unhealthyThresholdCount: 5,
});
// Configure auto scaling
const scaling = fargateService.service.autoScaleTaskCount({
minCapacity: 2,
maxCapacity: 10,
});
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 70,
scaleInCooldown: cdk.Duration.seconds(60),
scaleOutCooldown: cdk.Duration.seconds(60),
});
// Set the service URL
this.serviceUrl = `http://${fargateService.loadBalancer.loadBalancerDnsName}`;
// Outputs
new cdk.CfnOutput(this, 'StrapiServiceUrl', {
value: this.serviceUrl,
description: 'Strapi CMS service URL',
}) ;
new cdk.CfnOutput(this, 'StrapiDatabaseEndpoint', {
value: database.dbInstanceEndpointAddress,
description: 'Strapi database endpoint',
});
}
}
API Gateway Construct
The ApiGatewayConstruct
configures the API Gateway that exposes Lambda functions to the frontend.
// lib/api-gateway-construct.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
export interface ApiGatewayConstructProps {
semanticSearchLambda: lambda.Function;
orderProcessorLambda: lambda.Function;
inventoryManagerLambda: lambda.Function;
}
export class ApiGatewayConstruct extends Construct {
public readonly apiEndpoint: string;
constructor(scope: Construct, id: string, props: ApiGatewayConstructProps) {
super(scope, id);
// Create the API Gateway
const api = new apigateway.RestApi(this, 'ServerlessEcommerceApi', {
restApiName: 'Serverless E-commerce API',
description: 'API for serverless e-commerce',
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'],
allowCredentials: true,
},
});
// Create API resources
const productsResource = api.root.addResource('products');
const productResource = productsResource.addResource('{id}');
const searchResource = productsResource.addResource('search');
const ordersResource = api.root.addResource('orders');
const orderResource = ordersResource.addResource('{id}');
const inventoryResource = api.root.addResource('inventory');
// Integrate Lambda functions with API Gateway
const semanticSearchIntegration = new apigateway.LambdaIntegration(props.semanticSearchLambda);
const orderProcessorIntegration = new apigateway.LambdaIntegration(props.orderProcessorLambda);
const inventoryManagerIntegration = new apigateway.LambdaIntegration(props.inventoryManagerLambda);
// Configure API methods
productsResource.addMethod('GET', semanticSearchIntegration);
productResource.addMethod('GET', semanticSearchIntegration);
searchResource.addMethod('GET', semanticSearchIntegration);
searchResource.addMethod('POST', semanticSearchIntegration);
ordersResource.addMethod('POST', orderProcessorIntegration);
orderResource.addMethod('GET', orderProcessorIntegration);
inventoryResource.addMethod('GET', inventoryManagerIntegration);
inventoryResource.addMethod('POST', inventoryManagerIntegration);
// Set the API endpoint
this.apiEndpoint = api.url;
// Outputs
new cdk.CfnOutput(this, 'ApiGatewayUrl', {
value: this.apiEndpoint,
description: 'API Gateway URL',
});
}
}
Frontend Construct
The FrontendConstruct
configures the frontend hosting on Amazon S3 and CloudFront.typescript
// lib/frontend-construct.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as path from 'path';
export interface FrontendConstructProps {
apiEndpoint: string;
strapiEndpoint: string;
}
export class FrontendConstruct extends Construct {
public readonly distributionUrl: string;
constructor(scope: Construct, id: string, props: FrontendConstructProps) {
super(scope, id);
// Create an S3 bucket for frontend hosting
const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', {
websiteIndexDocument: 'index.html',
websiteErrorDocument: 'index.html',
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Only for development environment
autoDeleteObjects: true, // Only for development environment
});
// Create an Origin Access Identity for CloudFront
const originAccessIdentity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity');
websiteBucket.grantRead(originAccessIdentity);
// Create a CloudFront distribution
const distribution = new cloudfront.Distribution(this, 'Distribution', {
defaultRootObject: 'index.html',
defaultBehavior: {
origin: new origins.S3Origin(websiteBucket, {
originAccessIdentity,
}),
compress: true,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
errorResponses: [
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: '/index.html',
},
],
}) ;
// Create a configuration file for the frontend
const configFile = `
window.ENV = {
API_ENDPOINT: '${props.apiEndpoint}',
STRAPI_ENDPOINT: '${props.strapiEndpoint}'
};
`;
// Set the distribution URL
this.distributionUrl = `https://${distribution.distributionDomainName}`;
// Outputs
new cdk.CfnOutput(this, 'CloudFrontUrl', {
value: this.distributionUrl,
description: 'CloudFront distribution URL',
}) ;
new cdk.CfnOutput(this, 'BucketName', {
value: websiteBucket.bucketName,
description: 'S3 bucket name for frontend',
});
}
}
CDK Entry Point
Finally, we configure the CDK entry point that creates the main stack.
// bin/serverless-ecommerce-cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { ServerlessEcommerceStack } from '../lib/serverless-ecommerce-stack';
const app = new cdk.App();
new ServerlessEcommerceStack(app, 'ServerlessEcommerceStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
Deploying the Infrastructure
To deploy the infrastructure, run the following commands:
# Synthesize the CloudFormation template
cdk synth
# Deploy the stack
cdk deploy
By the end of this chapter, we've successfully assembled the complete infrastructure stack for Radixia Maca—from semantic search and headless content to inventory management and frontend delivery. Each component is modular, fully defined in code, and optimized for scalability through AWS services like Lambda, Fargate, API Gateway, and CloudFront. With CDK, we've transformed what could have been a sprawling manual setup into a unified, deployable blueprint for modern commerce. In the next part of our series, we'll dive into configuring Strapi CMS, where the content experience comes to life—connecting structured product data with flexible storytelling and media. Stay with us as we continue turning composable theory into operational reality.
In the next chapter, we'll see how to configure Strapi CMS to manage the content for our e-commerce.
Member discussion