@reapit-cdk/edge-api
TypeScript icon, indicating that this package has built-in type declarations

0.1.2 • Public • Published

@reapit-cdk/edge-api

npm version npm downloads coverage: 95.09%25 Integ Tests: ✔

This construct creates a truly globally available API where code executes at the edge. Because changes take a long time to propagate to all edge locations, there is a devMode flag which will instead deploy your API to a HTTP API. This is compatible with hotswapping, so cdk watch works very well. In order to make it easy to develop APIs which handle both event formats and work around the environment variable limitation, I recommend you use the lightweight request wrapper @reapit-cdk/edge-api-sdk which normalises the event format and offers some extra helpers.

Package Installation:

yarn add --dev @reapit-cdk/edge-api
# or
npm install @reapit-cdk/edge-api --save-dev

Usage

import { Stack, App } from 'aws-cdk-lib'
import { ARecord, HostedZone } from 'aws-cdk-lib/aws-route53'
import { Code, Runtime } from 'aws-cdk-lib/aws-lambda'
import { Bucket } from 'aws-cdk-lib/aws-s3'
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'

import {
  EdgeAPI,
  EdgeAPILambda,
  FrontendEndpoint,
  LambdaEndpoint,
  ProxyEndpoint,
  RedirectionEndpoint,
} from '@reapit-cdk/edge-api'
import { HeadersFrameOption } from 'aws-cdk-lib/aws-cloudfront'

const app = new App()

const stack = new Stack(app, 'stack-name', {
  env: {
    region: 'us-east-1', // region must be specified, and must be us-east-1 in production
  },
})

const certificate = new Certificate(stack, 'certificate', {
  domainName: 'example.org',
})

const api = new EdgeAPI(stack, 'api', {
  certificate,
  domains: ['example.org', 'example.com'],
  devMode: false, // optional, defaults to false
  defaultEndpoint: new ProxyEndpoint({
    destination: 'example.com',
    pathPattern: '/*',
  }),
})

const lambdaFunction = new EdgeAPILambda(stack, 'lambda', {
  code: Code.fromInline('export const handler = () => {}'),
  handler: 'index.handler',
  runtime: Runtime.NODEJS_18_X,
  environment: {
    aVariable: 'contents',
  },
})

api.addEndpoint(
  new LambdaEndpoint({
    pathPattern: '/api/lambda',
    lambdaFunction,
    isStatic: false, // optional, set to true to have the response cached until the next deployment
  }),
)

api.addEndpoint(
  new FrontendEndpoint({
    pathPattern: '/frontend', // this will route /frontend and /frontend/* to the bucket
    bucket: new Bucket(stack, 'bucket'),
    invalidationItems: ['/index.html', '/config.js'], // optional, (relative to the pathPattern), set to invalidate these paths after deployment
    // responseHeaderOverrides accepts https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.ResponseHeadersPolicyProps.html
    responseHeaderOverrides: {
      // Prevent the site from being iframed
      securityHeadersBehavior: {
        frameOptions: {
          frameOption: HeadersFrameOption.DENY,
          override: true,
        },
      },
    },
  }),
)

api.addEndpoint(
  new ProxyEndpoint({
    pathPattern: '/google',
    destination: 'google.com', // domain name only, will use https://

    // optional
    disableBuiltInMiddlewares: {
      // by default cookie domains will be rewritten from the destination (google.com)
      // to the domain the user requested (example.org or example.com)
      cookie: true,
      // by default redirects will be rewritten from the destination (google.com)
      // to the domain the user requested (example.org or example.com)
      redirect: true,
    },
  }),
)

api.addEndpoint(
  new ProxyEndpoint({
    pathPattern: '/search',
    destination: {
      'example.org': 'google.com',
      'example.com': 'bing.com',
    },
  }),
)

api.addEndpoint(
  new ProxyEndpoint({
    pathPattern: '/dynamic-search',

    destination: {
      'example.org': {
        destination: 'google.com',
        defaultSearchTerm: 'apples',
      },
      'example.com': {
        destination: 'bing.com',
        defaultSearchTerm: 'bananas',
      },
    },

    // optional
    customMiddlewares: [
      // @ts-expect-error
      (req, mapping) => {
        // req is CloudfrontRequest https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/common/cloudfront.d.ts#L44
        // mapping is destination['user requested domain name'] e.g. mapping['example.org']
        // req is mutable, modify it instead of returning a new object
        if (!req.querystring.includes('q=')) {
          if (typeof mapping !== 'string') {
            req.querystring = `q=${mapping.defaultSearchTerm}`
          }
        }
      },
    ],
  }),
)

// send the user to https://google.com/redirect-me
api.addEndpoint(
  new RedirectionEndpoint({
    pathPattern: '/redirect-me',
    redirect: true,
    destination: 'https://google.com',
  }),
)

const zone = HostedZone.fromLookup(stack, 'zone', {
  domainName: 'example.org',
})
new ARecord(stack, 'arecord', {
  zone,
  recordName: 'example.org',
  target: api.route53Target,
})

How is this different to an Edge-optimized API Gateway?

With Edge-optimized API Gateways, requests are routed from the user to the nearest CloudFront Point of Presence (POP), then to the region that the API resides in, where the request is handled.

With this construct, requests are routed from the user to the nearest CloudFront Point of Presence (POP) where they are handled and returned to the user, thus reducing latency and increasing availability.

Limitations

Environment Variables

Your configured environment variables will not be available in process.env. Instead, they exist in the lambda event. In production they are available on

JSON.parse(event.Records[0].cf.request.origin.s3.customHeaders.env[0].value)

and in development mode, in

JSON.parse(Buffer.from(event.headers.env, 'base64').toString('utf-8'))

Method Handling

Due to Cloudfront's AllowedMethods there are a few limitations:

  • Specifying multiple handlers for the same path is not currently possible.
  • Outside of specifying a GET & HEAD, or GET & HEAD & OPTIONS, you cannot limit which methods your handler will get called for. This means that if you have a POST handler, it'll get called for GET, HEAD, OPTIONS, PUT, PATCH, POST, and DELETE.

Lambda@Edge Restrictions

Lambda@Edge restrictions will apply to your lambdas. Your lambdas will be executed at origin request with includeBody set to true. The most important ones to note are:

  • VPCs are not supported
  • Execution duration must not exceed 30 seconds
  • Request body will be truncated on input and output to 1MB
  • Only NodeJS and Python is supported
  • Lambda containers are not supported
  • Lambda layers are not supported
  • ARM is not supported

Readme

Keywords

Package Sidebar

Install

npm i @reapit-cdk/edge-api

Weekly Downloads

72

Version

0.1.2

License

MIT

Unpacked Size

317 kB

Total Files

33

Last publish

Collaborators

  • joshbalfour