@connected-web/openapi-rest-api
v0.4.0
Published
An AWS-CDK library for building very cheap and extremely scalable OpenAPI documented APIs using AWS API Gateway, and AWS Lambda.
Downloads
341
Readme
OpenAPI Rest API
An AWS-CDK library for building very cheap and extremely scalable OpenAPI documented APIs using AWS API Gateway, and AWS Lambda.
Project Goals
- 🤔 Simplify API creation with AWS Lambda and API Gateway using OpenAPI 3.0.0. Enhance security with DNS records, SSL certificates, and Cognito User Pool.
- 🛠️ AWS CDK deploys resources, combining custom logic and OpenAPI in TypeScript. Utilizes API Gateway, AWS Lambda, and an authorizer lambda for security.
- 📦 Achieve a custom API on a hosted domain, easily deployable and recreatable with minimal ongoing costs.
Usage
To an existing CDK project, add the following dependency:
npm install @connected-web/openapi-rest-api
Example API
The examples folder demonstrates the skeleton for a practical project.
There should be at least four parts to each OpenAPI REST API project:
- 🌐 API Stack - extends
cdk.Stack
and implementsOpenAPIRestAPI<*>
- 📦 Resources* - extends
cdk.Construct
and contains shared resources to pass between endpoints - 📊 Models - extends
OpenAPIBasicModels
and contains the request and response data models for each endpoint - 🚀 Endpoints - extends
OpenAPIEndpoint
and contains the business logic for each endpoint
🌐 Example API Stack
The CDK stack is used to create the API Gateway, and to create the endpoints. The CDK stack should extend cdk.Stack
and implement OpenAPIRestAPI<*>
. The OpenAPIRestAPI<*>
interface requires the following parameters:
Description
- A description of the APISubDomain
- The subdomain that will be used to host the APIHostedZoneDomain
- The domain that will be used to host the APIVerifiers
- An array of verifiers that will be used to verify requests
The OpenAPIRestAPI<*>
interface also supports the following methods:
addEndpoints
- Adds an array of endpoints to the APIreport
- Reports the API Gateway URL and the OpenAPI specification URL
Example: ./ExampleAPI.ts
import * as cdk from 'aws-cdk-lib'
import { Construct } from 'constructs'
import { OpenAPIRestAPI, OpenAPIVerifiers, OpenAPIBasicModels } from '@connected-web/openapi-rest-api'
import { ExampleResources } from './Resources'
import { StatusEndpoint } from './endpoints/Status'
import { ReceivePayloadEndpoint } from './endpoints/ReceivePayload'
export interface IdentityConfig {
verifiers: OpenAPIVerifiers
}
export interface StackParameters { hostedZoneDomain: string, serviceDataBucketName: string, identity: IdentityConfig }
export class ExampleAPIStack extends cdk.Stack {
constructor (scope: Construct, id: string, props: cdk.StackProps, config: StackParameters) {
super(scope, id, props)
// Create shared resources
const sharedResources = new ExampleResources(scope, this)
// Create API Gateway
const apiGateway = new OpenAPIRestAPI<ExampleResources>(this, 'Example API', {
Description: 'Example API - https://github.com/connected-web/openapi-rest-api',
SubDomain: 'example-api',
HostedZoneDomain: config.hostedZoneDomain, // this hosted zone should already exist in the AWS account
Verifiers: config?.identity.verifiers ?? [], // provide a list of Cognito User Pool clients to verify against
// Optional alternative: AuthorizerLambdaArn: config?.identity.authorizerLambdaArn // supply a custom authorizer lambda via ARN
StackName: 'v1', // Default: 'v1'
AdditionalCORSHeaders: ['x-continuation-token'] // Defaults: ['Authorization', 'content-type']
}, sharedResources)
// Kick of dependency injection for shared models and model factory
OpenAPIBasicModels.setup(this, apiGateway.restApi)
// Add endpoints to API
apiGateway
.addEndpoints([
new StatusEndpoint(),
new ReceivePayloadEndpoint()
])
.report()
}
}
📦 Example Resources
The resources class is used to create shared resources that can be passed between endpoints. This is useful for resources that are used by multiple endpoints, such as a data bucket, or any other custom CDK resources supported by AWS.
Example: ./Resources.ts
import { Construct } from 'constructs'
import * as cdk from 'aws-cdk-lib'
import * as s3 from 'aws-cdk-lib/aws-s3'
export class ExampleResources {
scope: Construct
stack: cdk.Stack
constructor (scope: Construct, stack: cdk.Stack) {
this.scope = scope
this.stack = stack
}
get serviceDataBucket (): s3.Bucket {
const serviceBucketName = process.env.SERVICE_BUCKET_NAME ?? 'test-api-service-data-bucket'
return new s3.Bucket(this.stack, 'ExampleServiceBucket', {
bucketName: serviceBucketName,
removalPolicy: cdk.RemovalPolicy.DESTROY,
versioned: true
})
}
}
📊 Example Model
The models are used to define the request and response data for each endpoint. These models are also used to create the OpenAPI specification, which can then be used to validate the request and response data, as well as generate documentation and client libraries.
Example: ./models/ApiPayload.ts
import { IModel, JsonSchemaType, JsonSchemaVersion } from 'aws-cdk-lib/aws-apigateway'
import { OpenAPIBasicModels } from '@connected-web/openapi-rest-api'
export class ApiPayload extends OpenAPIBasicModels {
static get model (): IModel {
return OpenAPIBasicModels.modelFactory?.create('ApiPayload', {
schema: JsonSchemaVersion.DRAFT7,
title: 'Basic API Payload',
type: JsonSchemaType.OBJECT,
properties: {
operationId: {
type: JsonSchemaType.INTEGER,
description: 'The HTTP status code of the response'
},
payload: {
type: JsonSchemaType.OBJECT,
description: 'The data payload of the request'
}
},
required: ['operationId', 'payload']
}) as IModel
}
}
export interface ApiPayloadType {
operationId: number
payload: any
}
🚀 Example Endpoint
The API, Resources, and Models all come together in the endpoint. The endpoint is a TypeScript file that contains the business logic for the endpoint, and the metadata that describes the endpoint. The metadata is used by the CDK to create the stack that will host the endpoint, and is also used by the library to create the OpenAPI specification.
Example metadata: ./endpoints/Status/metadata.ts
import { Construct } from 'constructs'
import path from 'path'
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs'
import { MethodResponse } from 'aws-cdk-lib/aws-apigateway'
import { OpenAPIRouteMetadata } from '@connected-web/openapi-rest-api'
import { ExampleResources } from '../../Resources'
import { ApiResponse } from '../../models/ApiResponse'
/* This section is for route metadata used by CDK to create the stack that will host your endpoint */
export class StatusEndpoint extends OpenAPIRouteMetadata<ExampleResources> {
grantPermissions (scope: Construct, endpoint: NodejsFunction, resources: ExampleResources): void {
const serviceBucket = resources.serviceDataBucket
serviceBucket.grantRead(endpoint)
}
get operationId (): string {
return 'getStatus'
}
get restSignature (): string {
return 'GET /status'
}
get routeEntryPoint (): string {
return path.join(__dirname, 'handler.ts')
}
get lambdaConfig (): NodejsFunctionProps {
return {
environment: {
STATUS_INFO: JSON.stringify({
deploymentTime: process.env.USE_MOCK_TIME ?? new Date()
})
}
}
}
get methodResponses (): MethodResponse[] {
return [{
statusCode: '200',
responseParameters: {
'method.response.header.Content-Type': true,
'method.response.header.Access-Control-Allow-Origin': true,
'method.response.header.Access-Control-Allow-Credentials': true
},
responseModels: {
'application/json': ApiResponse.model
}
}]
}
}
Example handler: ./endpoints/Status/handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
import { httpStatusCodes, lambdaResponse } from '../../helpers/Response'
/* This handler is executed by AWS Lambda when the endpoint is invoked */
export async function handler (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
const statusInfo = process.env.STATUS_INFO ?? JSON.stringify({ message: 'No STATUS_INFO found on env' })
return lambdaResponse(httpStatusCodes.success, statusInfo)
}
🔐 API Authorization
By design, all endpoints are protected by an Authorization lambda.
By default, the library will create an authorizer lambda that verifies against a Cognito User Pool. This requires you to pass a list of verifiers to the API Gateway stack, which you will need to create and configure separately to this project. The idea being that you can reuse the same Cognito User Pool between multiple APIs.
All verifiers are checked in order, and the first verifier to return a valid response will be used to authorize the request. If no verifiers return a valid response, the request will be rejected. This allows you to have multiple environments, and to have different verifiers for each environment. For example, you could have a production environment with a production Cognito User Pool, and a development environment with a development Cognito User Pool.
{
"verifiers": [{
"name": "Users Dev",
"userPoolId": "eu-west-2_VBRbzaly6",
"tokenUse": "access",
"clientId": "123abc456def789ghi012jkl345",
"oauthUrl": "https://connected-web.example.auth.us-east-1.amazoncognito.com"
}, {
"name": "Apps Dev",
"userPoolId": "eu-west-2_VBRbzaly6",
"tokenUse": "access",
"clientId": "456def789ghi012jkl345123abc",
"oauthUrl": "https://connected-web.example.auth.us-east-2.amazoncognito.com"
}]
}
Alternatively, you can supply your own authorizer lambda by passing an ARN to the OpenAPIRestAPI constructor. This allows you to preconfigure, and reuse an existing authorizer between APIs - enabling a single sign-on experience for your users.
{
"AuthorizerARN": "arn:aws:lambda:us-east-3:123456789012:function:connected-web-shared-authorizer"
}
Testing
You will likely want pre-deployment tests in place to ensure your stack builds correctly.
See the tests/api-stack.test.ts file for an example.
Design Considerations
🤔 Simplify AWS Lambda API Creation with OpenAPI standards
Make it as simple as possible, but not simpler.
This library was created to make it easier to create a self-documenting API using AWS Lambda and API Gateway. The library uses the OpenAPI 3.0.0 specification to create the API Gateway Rest API, and uses AWS Lambda to create the endpoints. The library also creates the DNS records, SSL certificate, and Cognito User Pool to secure the API.
The architecture is designed to be as cheap as possible to run, whilst still being able to scale massively to meet demand. The code is designed to be as simple as possible to write, whilst still being able to handle complex business logic and support custom CDK resources.
🛠️ Unified AWS Resource Management with CDK
Things that change together should stay together.
The library uses the AWS CDK to create the AWS resources. The CDK is a framework for defining cloud infrastructure in code, and is written in TypeScript. The CDK is used to create the AWS resources, and the library is used to allow the custom application logic and the OpenAPI specification to be defined in a single TypeScript file for each endpoint.
The library creates the API Gateway as a Rest API, and uses AWS Lambda to create the endpoints. The library also creates the DNS records, SSL certificate, and an authorizer lambda which verifies against Cognito User Pool to secure the API.
📦 Low-effort AWS Resource Management for On-Demand Deployment
Easy to deploy, easy to destroy. Recreate from scratch at any time.
An API on a custom hosted zone:
- e.g. https://my-api.my-hosted-zone.com
By using this library, you will create and configure a large number of AWS resources as part of a Cloud Formation stack. This stack can be deployed and destroyed easily, and can be recreated from scratch at will. This means that you can easily create a new environment, or recreate an existing environment, at any time - and the costs are minimal. The only substantial ongoing costs are the storage costs for S3 and the hosting costs for the API Gateway.
Architecture
Here is a diagram of the architecture produced by the OpenAPI Rest API library:
graph TD
subgraph AWS[AWS Account]
subgraph CloudFormation[CloudFormation]
subgraph APIGateway[API Gateway]
subgraph Resources
subgraph Methods
Lambda
end
end
subgraph OpenAPI[OpenAPI 3.0]
subgraph Paths
OperationID
end
subgraph Models
subgraph Schemas
end
end
end
end
end
subgraph Lambda
subgraph Endpoints[Endpoints]
subgraph HandlerFunctions[Handlers]
subgraph HandlerFunction[Handler]
end
end
subgraph MetaData[Endpoint Data]
MD_PathSignature[Path Signature]
MD_OperationID[OperationID]
MD_Models[Models]
MD_Environment[Environment Vars]
end
end
subgraph OpenAPIRestAPI[OpenAPI Rest API]
subgraph Authorizer[Authorizer]
end
end
end
subgraph Route53[Route53]
subgraph HostedZone[Hosted Zone]
end
subgraph RecordSet[Record Set]
end
end
subgraph CertificateManager[Certificate Manager]
subgraph Certificate[Certificate]
end
end
subgraph CognitoUserPool[Cognito User Pool]
subgraph UserPool[User Pool]
end
end
subgraph SharedResources[Custom Resources]
S3Bucket[S3 Bucket]
AnyCDKResource[Any CDK Resource]
end
subgraph CloudWatch[CloudWatch]
subgraph CloudWatchLogGroup[Log Group]
end
subgraph CloudWatchMetrics[Metrics]
end
end
HandlerFunction -- read / write --> SharedResources
HandlerFunction -- logs to --> CloudWatch
APIGateway -- registered with --> RecordSet
RecordSet -- registered with --> HostedZone
APIGateway -- registered with --> Certificate
Certificate -- references --> RecordSet
Authorizer -- verifies against --> UserPool
end
Key architectural features
- 📋 AWS Account: The deployment location for the API.
- 🏗️ Cloud Formation: Service for API deployment.
- 🌐 API Gateway: Tool for API creation.
- 🚀 Lambda: Service for endpoint creation and business logic.
- 📄 OpenAPI: Specification for API description and documentation generation.
Component Key
| Component | Description | |------------------------|-------------------------------------------------------------------| | AWS Account | The AWS account where the API will be deployed. | | Cloud Formation | The AWS service that will be used to deploy the API. | | API Gateway | The AWS service that will be used to create the API. | | Lambda | The AWS service that will be used to create the endpoints. | | OpenAPI | The specification that will be used to describe the API. | | TypeScript File | The file that will be used to define the endpoints. | | Handler Functions | The functions that will be used to handle the requests; this is where you should write your custom business logic. | | Endpoint Meta Data | The data that will be used to define the endpoints; this data lives in the TypeScript file with the handler functions and is used to create the OpenAPI specification. | | OpenAPI Rest API | The library used to create the necessary resources for the API. | | Authorizer | The function that will be used to authorize requests. | | Route53 | The AWS service that will be used to create the DNS records. | | Hosted Zone | The pre-registered DNS zone that will be used to create the DNS records. | | Record Set | The DNS record that will map the API to a domain name. | | Certificate Manager | The AWS service that will be used to create the SSL certificate for the registered domain name. | | Certificate | The SSL certificate that will be used to secure the API. | | Cognito User Pool | An external authentication service that will be used to authorize requests. | | Custom Resources | Additional AWS resources that are custom to the application's needs. | | CloudWatch | The AWS service that will be used to access logs and metrics. | | CloudWatch Log Group | The log group that will be used to store logs from the handler functions when run via AWS Lambda. | | CloudWatch Metrics | The metrics for each lambda to track system activations and health. |
Environment Variables for CI
CREATE_CNAME_RECORD
If this environment variable is set to true
, the CDK stack will create a CNAME record in the hosted zone that points to the API Gateway. This is useful if you want to host the API on a custom domain. It is sometimes useful to toggle this when creating and destroying new resources to avoid CloudFormation conflicts.
OPENAPI_REST_API_REPORT_SUMMARY
If this environment variable is set to true
, the CDK stack will report the API Gateway URL and the OpenAPI specification URL. This is useful if you want to quickly access the API Gateway URL and the OpenAPI specification URL after deployment. It also summarises a table of endpoints, which is useful documentation for the API that you can copy and paste out of the GITHUB_STEP_SUMMARY
.
For example:
- name: Deploy the CDK Stack
run: npm run deploy -- --require-approval never
env:
CREATE_CNAME_RECORD: true
OPENAPI_REST_API_REPORT_SUMMARY: true
Contributions and Questions
Please raise an issue on GitHub if you have any questions, or if you would like to contribute to the project.
License
Released under ISC License
Copyright 2023 Connected Web
See LICENSE.md for more information.