Beta Acid logo

Avoid Repetition and Enforce Standards with Custom CDK Constructs in TypeScript

Development

Andre Santos

August 12, 2025

5 min read

Following on from our deep dive into managing Lambdas with AWS CDK, we talked about how CDK lets you define cloud infrastructure using actual code, not YAML. After shipping a few services, you’ll notice the same setup repeating: buckets, Lambdas, CloudFront, over and over. Different teams will solve the same problems slightly differently — and that’s how things drift.

Why not treat infra like real code? Abstract the repeated stuff into something clean and reusable. That’s where custom CDK constructs come in.

What Are L1, L2, and L3 Constructs?

You’ll often hear people talk about “L1,” “L2,” and “L3” constructs — and if you’re new to AWS CDK, you likely won't have any idea what they're talking about. These labels are shorthand for the level of abstraction you’re working with. The 'L's come in three flavors:

  • L1 constructs are basically direct wrappers around CloudFormation resources — think raw, no-frills building blocks. You control every little detail, but it can get verbose.
  • L2 constructs are nicer, more user-friendly abstractions — like Bucket or Function — with good defaults and helpers so you don’t have to configure everything from scratch.
  • L3 constructs are your own custom combos — bundles of L1s and L2s glued together into reusable patterns. It’s like building your own Lego sets that your whole team can use.

We’ll focus on creating and using an L3 construct to simplify a common infra pattern.

Building a static website with the CloudfrontS3Website construct

Implementation

This L3 custom construct sets up three things for you:

  • A secure S3 bucket configured with no public access.
  • A CloudFront distribution that serves your site over HTTPS with caching and compression.
  • A deployment process that uploads your static files and invalidates the cache automatically.

Here’s how it all comes together:

export interface CloudfrontS3WebsiteProps {
  bucketName: string;
  sourcePath: string;
}
export class CloudfrontS3Website extends Construct {
  public readonly bucket: Bucket;
  public readonly cloudfrontDistribution: Distribution;
  public readonly bucketDeployment: BucketDeployment;
  constructor(scope: Construct, id: string, props: CloudfrontS3WebsiteProps) {
    super(scope, id);
    const { bucketName, sourcePath } = props;
    this.bucket = new Bucket(this, bucketName, {
      bucketName,
      websiteIndexDocument: 'index.html',
      publicReadAccess: false,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });
    this.cloudfrontDistribution = new Distribution(this, `SiteDistribution-${bucketName}`, {
      defaultRootObject: 'index.html',
      minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
      defaultBehavior: {
        origin: S3BucketOrigin.withOriginAccessControl(this.bucket),
        compress: true,
        allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
        viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      },
    });
    this.bucketDeployment = new BucketDeployment(this, `BucketDeployment-${bucketName}`, {
      sources: [Source.asset(sourcePath)],
      destinationBucket: this.bucket,
      distribution: this.cloudfrontDistribution,
      distributionPaths: ['/*'],
    });
  }
}

Usage

Using this is as simple as it gets. Just drop it into your stack like this:

import { CloudfrontS3Website }  from '@andrelopesmds/cloudfront-s3-website'
new CloudfrontS3Website(this, 'MyStaticSite', {
  bucketName: 'my-bucket-name',
  sourcePath: './dist/'
})

And just like that — you get a secure static website with CloudFront and S3, all set up and deployed with minimal hassle.

Why This Matters

Building custom constructs like this isn’t just about saving a few lines of code. It’s about:

  • Consistency: Everyone on your team uses the same setup, so you don’t get a million slightly different buckets or CloudFront configs floating around.
  • Speed: Spin up new projects faster without rewriting the same infra over and over.
  • Safety: Secure defaults mean fewer mistakes and less time chasing down bugs caused by misconfiguration.
  • Maintainability: When you need to tweak or improve the setup, you do it in one place — and everyone benefits.

At the end of the day, it frees you and your team to focus on building features, not wrestling with infrastructure.

Final Thoughts

When your AWS CDK projects grow, you’ll spot patterns in how you build your infrastructure. Instead of repeating yourself (or letting teams do their own thing), build custom constructs to clean things up.

Start small, make it solid, and share it with your team. Before you know it, you’ll have a toolkit that saves time, reduces errors, and keeps everyone on the same page.

It’s a simple way to make infrastructure feel more like code — exactly what CDK is about. Your L3 custom constructs slash repetition, lock in standards, and most importantly, help you Get Shit Done.

Links:

SHARE THIS STORY

Get. Shit. Done. 👊

Whether your ideas are big or small, we know you want it built yesterday. With decades of experience working at, or with, startups, we know how to get things built fast, without compromising scalability and quality.

Get in touch

Whether your plans are big or small, together, we'll get it done.

Let's get a conversation going. Shoot an email over to projects@betaacid.co, or do things the old fashioned way and fill out the handy dandy form below.

Beta Acid is a software development agency based in New York City and Barcelona.


hi@betaacid.co

About us
locations

New York City

77 Sands St, Brooklyn, NY

Barcelona

C/ Calàbria 149, Entresòl, 1ª, Barcelona, Spain

London

90 York Way, London, UK

© 2025 Beta Acid, Inc. All rights reserved.