Angular 20 SSR with AWS CDK v2

In this article we’re going to set up an Angular 20 web application with server-side rendering, using AWS Lambda and Cloudfront to serve it, and AWS CDK v2 to manage the infrastructure.

If you’re reading this, I’m assuming that you know how CDK and Angular work. I’m not going to explain in detail how to set up your projects.

The CDK Construct

Create a CDK application wherever you want. I usually create a folder within my Angular project, called aws or infrastructure.

Now create a new file in the lib folder called angular-ssr.ts (or whatever you want) and put this code in it.

TypeScript
import {CfnOutput} from 'aws-cdk-lib';
import {Code, Function, FunctionUrl, FunctionUrlAuthType, HttpMethod, Runtime} from 'aws-cdk-lib/aws-lambda';
import {Construct} from 'constructs';
import {Certificate} from 'aws-cdk-lib/aws-certificatemanager';
import {
  CachePolicy,
  Distribution,
  OriginRequestPolicy,
  PriceClass,
  ViewerProtocolPolicy
} from 'aws-cdk-lib/aws-cloudfront';
import {FunctionUrlOrigin} from 'aws-cdk-lib/aws-cloudfront-origins';

export interface AngularSSRProps {
  /**
   * Used to comment the Cloudfront distribution
   */
  appName?: string;
  /**
   * If set, the Cloudfront distribution will have custom domains and an SSL certificate.
   * The certificate domains must cover the domains specified in `domainNames`.
   */
  certificate?: Certificate;
  /**
   * If set, the Cloudfront distribution will have custom domains.
   * For this to work, you must specify a `certificate` as well.
   */
  domainNames?: string[];
}

export class AngularSSR extends Construct {

  public distribution: Distribution;
  public lambdaUrl: FunctionUrl;
  public ssrLambda: Function;

  constructor(scope: Construct, id: string, props: AngularSSRProps) {
    super(scope, id);

    this.ssrLambda = new Function(this, 'WebsiteSSRLambda', {
      // Adjust this if it's outdated
      runtime: Runtime.NODEJS_22_X,
      handler: 'index.handler',
      // We'll create this file later, change path if needed
      code: Code.fromAsset(`./assets/ssr-lambda`),
      // Adjust to your project needs
      memorySize: 1024
    });

    this.lambdaUrl = this.ssrLambda.addFunctionUrl({
      authType: FunctionUrlAuthType.NONE,
      cors: {
        allowedOrigins: [
          'https://localhost:4200',
          ...(props.domainNames?.map(d => `https://${d}`) ?? []),
        ],
        allowedMethods: [HttpMethod.GET],
        allowCredentials: true,
        allowedHeaders: ["*"]
      }
    });

    this.distribution = new Distribution(this, 'WebsiteSSRDistribution', {
      certificate: props.certificate,
      domainNames: props.domainNames,
      defaultBehavior: {
        origin: new FunctionUrlOrigin(this.lambdaUrl),
        viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        originRequestPolicy: OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
        cachePolicy: CachePolicy.CACHING_OPTIMIZED
      },
      priceClass: PriceClass.PRICE_CLASS_ALL,
      comment: props.appName,
      errorResponses: [
        {httpStatus: 403, responseHttpStatus: 200, responsePagePath: '/index.html'},
        {httpStatus: 404, responseHttpStatus: 200, responsePagePath: '/index.html'}
      ]
    });

    new CfnOutput(this, 'ClientLambdaURL', {value: this.lambdaUrl.url});
    new CfnOutput(this, 'DistributionID', {value: this.distribution.distributionId});
  }

}

Some remarks on this code

  • Line 51: I’ve included https://localhost:4200 in the allowedOrigins property. This is only useful for debug purposes if you also plan to include additional API endpoints in your lambda, but it won’t be covered here.
  • Line 52: This line maps the domains array we’ll be using later, and adds “https://” to each of them, so that they can be used by CORS headers. If you also need old fashioned “http://“, you don’t.

Read more