import { CorsHttpMethod, DomainName, HttpApi, HttpApiProps, HttpMethod, HttpNoneAuthorizer, } from 'aws-cdk-lib/aws-apigatewayv2'; import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'; import { Construct } from 'constructs'; import { NodejsFunction, NodejsFunctionProps, } from 'aws-cdk-lib/aws-lambda-nodejs'; import { HttpLambdaIntegration, HttpLambdaIntegrationProps, } from 'aws-cdk-lib/aws-apigatewayv2-integrations'; import { LogGroup } from 'aws-cdk-lib/aws-logs'; import { CfnStage } from 'aws-cdk-lib/aws-apigatewayv2'; import { ServicePrincipal } from 'aws-cdk-lib/aws-iam'; import { ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53'; import { ApiGatewayv2DomainProperties } from 'aws-cdk-lib/aws-route53-targets'; import { CfnOutput } from 'aws-cdk-lib'; import { HttpLambdaAuthorizer, HttpLambdaResponseType, } from 'aws-cdk-lib/aws-apigatewayv2-authorizers'; import { getCdkEnv } from './env'; import { lambda } from './lambda'; type HttpEndpoint = `${HttpMethod} /${string}`; type HttpEndpoints = Record< TEndpoint, string | HttpEndpointOptions >; type HttpEndpointOptions = NodejsFunctionProps & { integration?: HttpLambdaIntegrationProps; publicEndpoint?: boolean; }; /** * A HTTP API backed with lambdas, using a `{stackId}.mydomain.com` domain or `mydomain.com` for production */ export class ApiConstruct extends Construct { public readonly httpApi: HttpApi; constructor(scope: Construct, id: string) { super(scope, id); const env = getCdkEnv(); // You'll have to create this auth handler yourself const authorizeFn = lambda(this, 'authorize', { entry: 'src/handlers/http/auth/authorize.ts', }); const authorizer = new HttpLambdaAuthorizer('authorizer', authorizeFn, { responseTypes: [HttpLambdaResponseType.SIMPLE], }); const httpApiProps: HttpApiProps = { corsPreflight: { allowHeaders: ['*'], allowMethods: [CorsHttpMethod.ANY], allowOrigins: [], // TODO: set your allowed origins here }, defaultAuthorizer: authorizer, }; if (env.dns) { const domainName = env.dns.hostedZoneName; // May not be correct for your stack const dn = new DomainName(this, `${id}-dn`, { domainName, certificate: Certificate.fromCertificateArn( this, `${id}-cert`, env.dns.certificateArn ), }); this.httpApi = new HttpApi(this, `${id}-api`, { ...httpApiProps, defaultDomainMapping: { domainName: dn, }, }); const hz = HostedZone.fromHostedZoneAttributes(this, `${id}-hz`, { hostedZoneId: env.dns.hostedZoneId, zoneName: env.dns.hostedZoneName, }); new ARecord(this, `${id}-arecord`, { target: RecordTarget.fromAlias( new ApiGatewayv2DomainProperties( dn.regionalDomainName, dn.regionalHostedZoneId ) ), zone: hz, }); } else { this.httpApi = new HttpApi(this, `${id}-api`, httpApiProps); new CfnOutput(this, `${id}-endpoint`, { value: this.httpApi.apiEndpoint, }); } } /** * Adds a map of endpoints + handlers to the API, with a range of default options * @example * ```ts * // Adds a single "Hello world" endpoint to "/", with a `GET` HTTP verb, running on X86 CPU architecture * addEndpointsWithDefaults({ * "GET /": "src/helloWorld.ts" * }, { * architecture: Architecture.X86_64 * }) * * // Adds a single "Hello world" endpoint to "/", with a `GET` HTTP verb, running on ARM CPU architecture * addEndpointsWithDefaults({ * "GET /": { * entry: "src/helloWorld.ts", * architecture: Architecture.ARM_64, * } * }, { * architecture: Architecture.X86_64 * }) * ``` */ addEndpointsWithDefaults( endpoints: HttpEndpoints, defaultOptions: Partial ) { const mapped = Object.fromEntries( Object.entries(endpoints).map(([endpointKey, props]) => { const handlerProps: HttpEndpointOptions = typeof props === 'string' ? // Map to default options { entry: props, } : (props as HttpEndpointOptions); const fn = this.addEndpoint(endpointKey as HttpEndpoint, { ...defaultOptions, ...handlerProps, }); return [endpointKey, fn]; }) ); return mapped as Record; } /** * Adds a map of endpoints + handlers to the API, authenticated by default * @example * ```ts * // Adds a single "Hello world" endpoint to "/", with a `GET` HTTP verb * addEndpoints({ * "GET /": "src/helloWorld.ts" * }) * * // Adds a single "Hello world" endpoint to "/", with a `GET` HTTP verb, running on X64 CPU architecture * addEndpoints({ * "GET /": { * entry: "src/helloWorld.ts", * architecture: Architecture.X86_64, * } * }) * ``` */ addEndpoints(endpoints: HttpEndpoints) { return this.addEndpointsWithDefaults(endpoints, {}); } /** * Adds a map of unauthenticated endpoints + handlers to the API * @example * ```ts * // Adds a single unauthenticated "Hello world" endpoint to "/", with a `GET` HTTP verb * addPublicEndpoints({ * "GET /": "src/helloWorld.ts" * }) * * // Adds a single unauthenticated "Hello world" endpoint to "/", with a `GET` HTTP verb, running on X64 CPU architecture * addPublicEndpoints({ * "GET /": { * entry: "src/helloWorld.ts", * architecture: Architecture.X86_64, * } * }) * ``` */ addPublicEndpoints( endpoints: Record< TEndpoints, string | Omit > ) { return this.addEndpointsWithDefaults(endpoints, { publicEndpoint: true, }); } /** * Adds an endpoint + handler to the API, authenticated by default * @example * ```ts * // Adds a single "Hello world" endpoint to "/", with a `GET` HTTP verb * addEndpoint("GET /", "src/helloWorld.ts") * * // Adds a single "Hello world" endpoint to "/", with a `GET` HTTP verb, running on X64 CPU architecture * addEndpoint("GET /", { * entry: "src/helloWorld.ts", * architecture: Architecture.X86_64, * }) * ``` */ addEndpoint(endpoint: HttpEndpoint, props: HttpEndpointOptions) { const newLambda = lambda(this, `${endpoint}-fn`, props); const integration = new HttpLambdaIntegration( `${endpoint}-integration`, newLambda, props.integration ); const endpointParts = endpoint.split(' '); if (endpointParts.length !== 2) { throw new Error(`Invalid endpoint key ${endpoint}`); } const [httpMethod, endpointPath] = endpointParts; this.httpApi.addRoutes({ integration, path: endpointPath, authorizer: props.publicEndpoint ? new HttpNoneAuthorizer() : undefined, methods: [httpMethod as HttpMethod], }); return newLambda; } }