jonelantha: Blog

How-To: Host serverless AWS Api Gateway and static files on the same domain

9th September 2020

This post contains an example of how to setup Serverless Framework to deploy static front-end files and a serverless api behind the same domain name. Sounds simple but there's a little bit of extra config involved...

Most of the config in this article is a CloudFormation template; so it should also be useful for SAM based projects.

Serverless Simplicity...

  • Serve the front-end static files from
  • Serve the lambda function powered API Gateway from

With only one host, cross-origin issues should be minimised, no need to configure CORS headers or tweak Cookie settings.

But wait - isn't it best practice to use as many domains as possible?

A few years ago it was considered a good idea to use Domain Sharding (having the site's resources split over multiple domains). Browsers limit themselves to 6 concurrent HTTP/1.1 connections (per host) and so using more hosts meant that limit could be increased.

However, modern browsers and HTTP/2 on the server make this practice obselete.

Still, it's important to note that the single domain approach outlined here isn't for all projects, particularly larger projects or projects which share resources with other sites.

The Stack

Here's what the example stack provides:

  • CDN
    • Site served using CloudFront
    • Supports HTTP/2
    • Secured with the latest version of the SSL protocol (all insecure http requests will be redirected to https)
    • CloudFront cache will be invalidated after each deployment
  • Static Files
    • Static files stored in an S3 Bucket, served via CloudFront (the best practice way to serve static content with the lowest costs)
    • Bucket content is private (unless accessed via the allocated domain name), i.e. no S3 website hosting
    • Static content served with default CloudFront cache settings
    • The S3 bucket is created as part of the stack
  • API
    • API supplied by HTTP API Gateway (also known as API Gateway V2)
    • Hosted using the same domain as the static content
    • No caching by default but caching can be configured per end-point by sending appropriate headers from the lambda functions
  • Domain Name
    • Domain Name allocated as part of the stack, so it should be straightforward to have different domains for different stages (or different developers)

This example assumes the following:

The settings in the custom section will need to be tweaked:

  • hostedZoneName: The Route53 hosted zone of the domain name, don't forget the . at the end!
  • domainName: The sub-domain to create. Hard-coded here for simplicity but could be adapted to be dependent on the stage
  • certificateARN: The ARN of an ACM certificate which will cover the domain name (usually a wildcard certificate). This must be in the us-east-1 region regardless of the stack's region


So here's the serverless.yml file, it's for Serverless Framework but could be adapted for Amazon's Serverless Application Model.

Some notes inline:

service: singleDomainProject

  name: aws
  runtime: nodejs12.x
    payload: "2.0"

  # configure the following (see above for details)
  certificateARN: "arn:aws:acm:us-east-1:############:certificate/########-####-####-####-############"
  # bucket name is based on the domain, tweak this as appropriate
  bucketName: ${self:custom.domainName}
    - bucketName: ${self:custom.bucketName}
      localDir: dist # where to copy the static files from
    distributionIdKey: CloudFrontId
    items: ["/*"]

  - serverless-s3-sync
  - serverless-cloudfront-invalidate

    - node_modules/**
    - dist/**
    - package.json
    - package-lock.json

# hello world api, code not included here
    handler: handler.hello
      - httpApi:
          path: /api/hello
          method: get

      Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
          Comment: !Join ["-", ["access-identity", !GetAtt S3Bucket.DomainName]]
      Type: AWS::CloudFront::Distribution
          PriceClass: PriceClass_100
            - ${self:custom.domainName}
          Enabled: true
          HttpVersion: http2
          DefaultRootObject: index.html
            AcmCertificateArn: ${self:custom.certificateARN}
            SslSupportMethod: sni-only
            MinimumProtocolVersion: TLSv1.2_2019
            - Id: api
              DomainName: !Join [".", [!Ref HttpApi, "execute-api", !Ref AWS::Region, ""]]
                OriginProtocolPolicy: https-only
            - Id: static
              DomainName: !GetAtt S3Bucket.RegionalDomainName
                OriginAccessIdentity: !Join ["", ["origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity]]
            - PathPattern: api/*
              TargetOriginId: api
              # recently deprecated style of setting ForwardedValues
              # but currently there's no alternative if using CloudFormation
                QueryString: true
              ViewerProtocolPolicy: redirect-to-https
              AllowedMethods: [GET, HEAD, OPTIONS, PATCH, POST, PUT, DELETE]
              # No caching by default but caching can be set with appropriate headers in the api responses
              MinTTL: 0
              DefaultTTL: 0
            TargetOriginId: static
            # 'Managed-CachingOptimized'
            CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
            # 'Managed-CORS-S3Origin'
            OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf
            ViewerProtocolPolicy: redirect-to-https
      Type: AWS::S3::Bucket
        BucketName: ${self:custom.bucketName}
      Type: AWS::S3::BucketPolicy
        Bucket: !Ref S3Bucket
            - Effect: Allow
              Action: "s3:GetObject"
              Resource: !Join ["", [!GetAtt S3Bucket.Arn, "/*"]]
                CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
      Type: AWS::Route53::RecordSetGroup
        HostedZoneName: ${self:custom.hostedZoneName}
        - Name: ${self:custom.domainName}
          Type: A
            DNSName: !GetAtt CloudFrontDistribution.DomainName
            HostedZoneId: Z2FDTNDATAQYW2 # Hard-coded to the hosted zone of CloudFront
      Description: CloudFront Id
      Value: !Ref CloudFrontDistribution
        Name: CloudFrontId

Looks great, anything else I might like to look at?

Building the front-end

This example doesn't build the front-end, typically npm scripts would be setup to build the front-end and then deploy it using serverless deploy

Local dev

To develop locally, serverless-offline will serve the api gateway but won't serve the static files. Typically front-ends are developed using a local web server - this local server could be configured to proxy any requests for /api/* on to the local api server (or on to an api already deployed to AWS). Taking create-react-app as an example, here's some information on how to setup the proxy.


The above stack just uses the CloudFront caching defaults. Ideally the serverless-s3-sync plugin would be configured to set the appropriate cache headers for different front-end files:

  • files which always have the same name (index.html etc) should not be cached in the browser: CacheControl: 'no-cache'
  • files which are always deployed with a different name (js or css files with hashed names) can be cached for much longer in the browser: CacheControl: 'public, max-age=31536000'

But are there any alternatives to this approach?

It's also worth considering the following alternatives:

  • Just use separate domains for the front-end and the api (and keep your config smaller) 🙂
  • Next.js - next generation React framework with serverless option
  • Serving static files from a lambda - a single Api Gateway also serves static content

Thanks for reading!

© 2003-2021 jonelantha