Avoid Repetition and Enforce Standards with Custom CDK Constructs in TypeScript
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 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.