DEV Community

Akifumi Niida for AWS Community Builders

Posted on

Tips for exposing SPAs and APIs with Cloudfront + S3 + API Gateway

This is a common pattern, but I would like to share some of the practices that I have arrived at.


Conclusion first

  • SPA: Change path with extension to /index.html using Cloudfront Functions
  • API: Point /api to API Gateway with Behavior


Until now, when publishing SPA applications such as Vue or Angular with Cloudfront + S3
S3 returns 403 or 404 by making a request to a path that does not exist.
Therefore, we had to create a custom error page on the Cloudfront side and configure it to redirect to /index.html.

Common problem 1 (SPA path issue)

Even if there is a 404 that should be caught, it is redirected without care.
For example, it is hard to notice if there is a CSS or JS upload error.

Common Problem 2 (Cloudfront problem of assigning specific paths)

I want to assign /api paths to API Gateway.
This method seems to be good from a security point of view, as it eliminates the need to configure CORS.

SPA path problem

Using Cloudfront Functions, I can distribute requests like /users/xxx to /index.html in a good way without waiting for error pages.

  • Cloudfront Functions will be the ones you code to handle events at the edge.
  • There is a lambda@edge that is similar, but you can read more about the differences here for the difference.
  • It is better to understand that lambda@edge has limited functionality and can only perform simple processing.

Also, if you want to try Cloudfront Functions quickly, you can try this.
Cloudfront Functions tutorial

terraform example

Configuring Cloudfront Functions

Define resources like this. (Parts not directly related to AWS Provider settings, etc., are omitted.)
See documentation for a description of the parameters.

  • Write the actual process in code. Here, it is read from a separate file.
  • You can make it public by setting publish = true.
resource "aws_cloudfront_function" "spa_redirect" {
  name = "${var.product_name}-${var.env}-spa-redirect"
  runtime = "cloudfront-js-1.0"
  comment = "${var.product_name}-${var.env}-spa-redirect"
  publish = true
  code = file("cloudfront_functions/spa-redirect.js")
Enter fullscreen mode Exit fullscreen mode

function part

It is written in a very old fashioned way, but for some reason it needs to be ECMAScript 5.1 compliant.
I would be very grateful if you could improve on this.

  • The presence or absence of extensions is determined by the presence or absence of a dot. if(request.uri.indexOf(".")) === -1)
var index = '/index.html';
function handler(event) {
    var request = event.request;
    // if extension not found (access not real file)
    if(request.uri.indexOf(".")) === -1) {
        request.uri = index;
    return request;
Enter fullscreen mode Exit fullscreen mode

Configuring Cloudfront Functions to adapt to Cloudfront

This is a long list, but all you need to focus on is the function_association at the bottom.

  • You can set it for each behavior!
  • The event_type is at the time of request, so we'll write viewer-request.
  • For more information on event_type, please refer to here for more information about event_type. Cloudfront Functions supports only viewer-request and viewer-response.
resource "aws_cloudfront_distribution" "front" {
  origin {
    domain_name = aws_s3_bucket.front.bucket_regional_domain_name
    origin_id = local.s3_origin_id

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.origin.cloudfront_access_identity_path
  enabled = true
  is_ipv6_enabled = true
  comment = "${var.product_name}-${var.env}"
  default_root_object = "index.html"
  aliases = ["test.${}"]

  default_cache_behavior {
    allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]]
    cached_methods = ["GET", "HEAD"].
    target_origin_id = local.s3_origin_id

    forwarded_values {
      query_string = true

      cookies {
        forward = "none"

    function_association {
      event_type = "viewer-request"
      function_arn = aws_cloudfront_function.spa_redirect.arn

    viewer_protocol_policy = "allow-all"
    min_ttl = 0
    default_ttl = 0
    max_ttl = 0

... Omitted.
Enter fullscreen mode Exit fullscreen mode

I want to separate /api

As mentioned above, you can use cloudfront's behavior to separate them.
Many people create API Gateways using the serverless framework or CDK.
It is convenient to get values from Cloudformation.


  • Get the endpoint and stage from the cloudformation in the domain_name of origin.
  • Specify api/* in path_pattern of behavior.
  • Since /api is not needed to access the APIGateway, it is removed by lambda@edge (this could also be replaced by cloudfront functions).
resource "aws_cloudfront_distribution" "front" {
  origin {
    custom_origin_config {
      http_port = "80"
      https_port = "443"
      origin_protocol_policy = "https-only"
      origin_ssl_protocols = ["TLSv1.2"]
    # Extract and v1 from ServiceEndpoint
    domain_name = split("/", data.aws_cloudformation_stack.api_test.outputs["ServiceEndpoint"])[2])
    origin_id = var.product_name
  ordered_cache_behavior {
    path_pattern = "api/*"
    allowed_methods = ["HEAD", "DELETE", "POST", "GET", "OPTIONS", "PUT", "PATCH"].
    cached_methods = ["GET", "HEAD"].
    target_origin_id = var.product_name

    forwarded_values {
      headers = ["Authorization"].
      query_string = true
      cookies {
        forward = "none"

    min_ttl = 0
    default_ttl = 10
    max_ttl = 10
    viewer_protocol_policy = "https-only"
    lambda_function_association {
      event_type = "origin-request"
      lambda_arn = aws_lambda_function.redirect_trim_context.qualified_arn

Enter fullscreen mode Exit fullscreen mode

That's all, obvious! Some of you may think it is, but we hope it will help you if you have any doubts when configuring your infrastructure.

SPA routing process with AWS CloudFront Functions

Top comments (0)