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:

  1. Main Stack: Coordinates all constructs and defines dependencies
  2. API Gateway Construct: Manages REST APIs for the frontend
  3. Bedrock Search Construct: Configures semantic search with Bedrock
  4. Data Storage Construct: Manages DynamoDB and other storage services
  5. Frontend Construct: Configures frontend hosting
  6. MCP Server Construct: Implements the Model Context Protocol server
  7. 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.

Share this post