Skip to content
ExponaBLOG
AWS FOR DEVELOPERS · DAY 065 / 100

CDK in TypeScript: what every AWS developer should know

Infrastructure with types and autocomplete.

Expona Admin6 min read

When you're managing infrastructure across multiple AWS accounts and environments, manually clicking through the console becomes a liability fast. CDK in TypeScript lets you define your entire AWS infrastructure as type-safe code, giving you autocomplete, compile-time errors, and the ability to treat your infrastructure like any other codebase—with tests, reviews, and version control. After five years building production systems, I've found that this shift from point-and-click to infrastructure-as-code with strong typing prevents more bugs than any manual review process could catch.

Constructs bundle best practices

The fundamental building block in CDK is the construct. Think of it as a reusable component that encapsulates AWS resources and their configuration according to best practices. Instead of creating an S3 bucket and then remembering to enable versioning, encryption, and block all public access separately, you package those decisions into a construct once and reuse it everywhere.

Here's a practical example from a recent e-commerce platform we built:

typescript
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cdk from 'aws-cdk-lib';

export class SecureUploadBucket extends cdk.Stack {
  public readonly bucket: s3.Bucket;

  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    this.bucket = new s3.Bucket(this, 'Uploads', {
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      versioned: true,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      lifecycleRules: [
        {
          expiration: cdk.Duration.days(90),
          noncurrentVersionExpiration: cdk.Duration.days(30),
        },
      ],
    });
  }
}

Notice that we've baked in encryption, public access blocking, versioning, and lifecycle management into a single construct. Every team member who uses this construct gets these protections automatically—no configuration burden, no forgetting a critical security setting. The TypeScript type system ensures that only valid encryption options or retention policies can be passed, preventing runtime surprises.

I've seen teams ship constructs that enforce organizational standards: IAM roles with least-privilege policies, VPCs with standard subnetting patterns, RDS instances with automated backups and encryption. The construct becomes the source of truth for how that piece of infrastructure should look.

Synthesizes to CloudFormation

CDK doesn't run directly on AWS. Instead, it synthesizes your TypeScript code down to CloudFormation templates—AWS's native infrastructure-as-code language. This is a crucial distinction because it means your code is actually declarative, idempotent, and auditable.

When you run cdk synth, your TypeScript constructs are translated into JSON CloudFormation:

bash
cdk synth

This produces a CloudFormation template in the cdk.out directory. You can inspect this template before deployment, commit it to version control, or feed it into compliance scanning tools. On a recent CRM platform, our security team required that all CloudFormation be reviewed before deployment—CDK made this possible while keeping the code-side developer experience clean.

The synthesis step also means your infrastructure can be deployed via the AWS CLI or through CI/CD pipelines without any CDK dependencies in the runtime environment. Your build system synthesizes once, and then CloudFormation takes over:

bash
cdk synth && aws cloudformation deploy --template-file cdk.out/MyApp.json

I often use this flow in production: commit TypeScript code, trigger synthesis in CI, scan the resulting template for security issues, manually approve if needed, then deploy. The separation between code and deployment artifact gives you checkpoints without compromising velocity.

Unit test your infrastructure

This is where CDK's TypeScript foundation pays real dividends. Your infrastructure code is real code, which means you can write unit tests against it before ever touching AWS. No more "I think this will work" deployments.

Using CDK's built-in assertions library, you can verify that your constructs produce the expected CloudFormation:

typescript
import { Template } from 'aws-cdk-lib/assertions';
import * as cdk from 'aws-cdk-lib';
import { SecureUploadBucket } from '../lib/secure-upload-bucket';

describe('SecureUploadBucket', () => {
  it('should create an encrypted, versioned bucket with public access blocked', () => {
    const app = new cdk.App();
    const stack = new SecureUploadBucket(app, 'TestStack');

    const template = Template.fromStack(stack);

    // Assert the bucket exists and has the right properties
    template.hasResourceProperties('AWS::S3::Bucket', {
      VersioningConfiguration: {
        Status: 'Enabled',
      },
      BucketEncryption: {
        ServerSideEncryptionConfiguration: [
          {
            ServerSideEncryptionByDefault: {
              SSEAlgorithm: 'AES256',
            },
          },
        ],
      },
      PublicAccessBlockConfiguration: {
        BlockPublicAcls: true,
        BlockPublicPolicy: true,
        IgnorePublicAcls: true,
        RestrictPublicBuckets: true,
      },
    });
  });
});

On our trading platform, we caught a misconfiguration in a Lambda execution role's trust policy before it ever hit staging—a test flagged that we weren't allowing the intended service to assume the role. That's the kind of fast feedback loop that compounds over time.

Unit tests also serve as living documentation. When a junior developer asks "why is this encrypted this way," the test answers the question and makes the design intent explicit.

Share constructs across teams

Once you've built a solid construct, packaging it for reuse across your organization becomes straightforward. You can publish CDK constructs as npm packages, version them, and depend on them like any other library.

We built a shared constructs library for our platform team that included: secure database stacks, load-balanced application stacks, observability patterns, and VPC configurations. Each team that spun up a new microservice could import these constructs and instantiate infrastructure in minutes:

typescript
import * as cdk from 'aws-cdk-lib';
import { SecureRDSStack, LoadBalancedAppStack } from '@company/cdk-constructs';

export class MyServiceStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const database = new SecureRDSStack(this, 'Database', {
      engine: 'postgres',
      backupRetentionDays: 30,
    });

    const app = new LoadBalancedAppStack(this, 'App', {
      containerImage: 'my-service:latest',
      databaseSecret: database.secret,
    });
  }
}

The shared library enforced organizational standards while giving individual teams autonomy. When the security team decided to add additional encryption or compliance tagging, a single update to the library propagated to all consumers—no individual team had to remember to make the change.

Versioning is critical here. We used semantic versioning and maintained a changelog so that teams could upgrade on their own schedule. A breaking change meant a major version bump, giving teams time to plan migrations.

Common mistakes

  • Over-parameterizing constructs. I've seen teams create a single "universal" construct that accepts 50 configuration options. This defeats the purpose. Better to have three opinionated constructs for three common patterns and let developers extend them if truly needed.
  • Skipping synthesis review. It's tempting to trust the CDK and deploy directly. Always run cdk diff before cdk deploy and inspect what CloudFormation changes will occur. Surprises at deploy time are expensive.
  • Not testing constructs in isolation. Unit tests are fast and cheap. If you only test constructs integrated into the full stack, you'll miss simple bugs and spend more time debugging.
  • Ignoring removal policies. Databases and buckets shouldn't vanish when you delete a stack. Use RemovalPolicy.RETAIN by default for anything that holds data.
  • Hardcoding values. Magic strings for AMI IDs, certificate ARNs, or VPC IDs make your constructs fragile. Use context values or properties to parameterize them.

Key takeaways

  • Constructs encapsulate AWS resources and best practices, reducing configuration burden and preventing mistakes through reusability.
  • TypeScript's type system catches configuration errors at compile time, not at 3 a.m. during a production incident.
  • CDK synthesizes to CloudFormation, giving you a standard, auditable deployment artifact and integration points with compliance tooling.
  • Unit tests let you verify infrastructure behavior before touching AWS, shortening feedback loops dramatically.
  • Shared construct libraries scale organizational standards without imposing rigid top-down control—teams retain flexibility while staying within guardrails.
  • Always review cdk diff output before deployment and use removal policies explicitly to protect data.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles