npm package: @orange-stack/openapi-lambda
Description: A CDK construct for Typescript that can be used to create the AWS infrastructure to support a API Gateway to Lambda integration based on an OpenAPI specification.
Author: OrangeStack
Latest version: 0.7.1

# OrangeStack OpenAPI contract first API Gateway + Lambda construct library

A CDK construct for Typescript that can be used to create the AWS infrastructure to support a API Gateway to Lambda integration based on an OpenAPI specification.

When CDK commands are executed all boilerplate code for API endpoints implementation is also created abstracting this complexity from the developer and letting him focus only on service funcionality implementation.

## Prerequisites

- NodeJS 14.x - <https://nodejs.org>
- AWS account properly configured - <https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html>
- AWS CDK CLI - <https://docs.aws.amazon.com/cdk/latest/guide/cli.html>

## Optional (Recommended) development tools

- Visual Studio Code (IDE) - <https://code.visualstudio.com/>
- Prettier VSCode extension (Code formatter) - <https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode>
- Jest Runner VSCode extension (Runs jest tests from IDE) - <https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner>
- OpenAPI (Swagger) Editor VSCode extension (OpenAPI intellisense and documentation preview) - <https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi>
- StopLight Spectral VSCode extension (OpenAPI linter) - <https://marketplace.visualstudio.com/items?itemName=stoplight.spectral>
- StopLight Studio (OpenAPI visual editor) - <https://stoplight.io/studio/>

## How to build library package

- To build the library package clone the repository and run npm scripts

```
git clone https://github.com/alexandrefvb/orangestack-cdk-openapi-lambda.git
cd orange-stack-cdk-openapi-lambda
npm install
npm run build
npm run package
```

- The library will be built and package `openapi-lambda@<version>.jsii.tgz` will be generated in `dist/js` folder

## Quick start

The below example will generate a simple hello world service to ilustrate the library usage.

1. Init a CDK app using the cli

```
mkdir hello-world-service
cd hello-world-service
cdk init --language=typescript
```

2. Add `openapi-lambda@<version>.jsii.tgz` and `@types/aws-lambda` as project dependencies

```
npm install @types/aws-lambda <orangestack-cdk-openapi-lambda location>/dist/js/openapi-lambda@<version>.jsii.tgz
```

3. Create file `hello-world-service/spec/hello-world.yaml` with the OpenAPI specification of hello world service

```yaml
openapi: 3.0.3
info:
  title: hello-world
  version: '1.0'
  description: A simple hello-world REST service
  contact:
    email: alexandre.bitencourt@zup.com.br
servers:
  - url: 'http://localhost:3000'
tags:
  - name: hello
    description: Hello world services
paths:
  /hello:
    post:
      tags:
        - hello
      description: Receive a name in request body and respond with a greeting message for the name informed
      operationId: post-hello
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HelloResponse'
              examples:
                example-1:
                  value:
                    greeting: Hello John!
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HelloRequest'
            examples:
              example-1:
                value:
                  name: John
components:
  schemas:
    HelloRequest:
      title: HelloRequest
      type: object
      properties:
        name:
          type: string
      required:
        - name
    HelloResponse:
      title: HelloResponse
      type: object
      properties:
        greeting:
          type: string
      required:
        - greeting
```

4. Edit the stack definition `hello-world-service/lib/hello-world-service-stack.ts`, import `OrangeStackOpenApiServices` and create the construct pointing to service spec yaml

```javascript
import * as cdk from '@aws-cdk/core';
// import OrangeStackOpenApiServices
import { OrangeStackOpenApiServices } from '@orangestack-cdk/openapi-lambda';

export class HelloWorldServiceStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Create OrangeStackOpenApiServices pointing to spec file
    new OrangeStackOpenApiServices(this, 'HelloWorldServiceApi', {
      specPath: 'spec/hello-world.yaml',
    });
  }
}
```

5. Execute CDK bootstrap to prepare stack and generate service stubs

```
cdk bootstrap --profile <your-aws-profile>
```

If you have permissions problems related to S3 public access block configuration permissions on bootstrap you could add the option `--public-access-block-configuration false` to the bootstrap command as shown below:

```
cdk bootstrap --profile <your-aws-profile> --public-access-block-configuration false
```

After bootstrap service controller and usecase stubs will be generated at path `src/post-hello`.

6. Edit generated usecase stub (`src/post-hello/usecase.ts`) and implement the code to generate the expected response imported from `src/api-models.ts` as shown below:

```javascript
import { HelloRequest, HelloResponse } from '../api-schemas';

export type PostHelloParams = {
  requestBody: HelloRequest,
};

export const postHello = async ({ requestBody }: PostHelloParams): Promise<HelloResponse> => {
  return {
    greeting: `Hello ${requestBody.name}!`,
  };
};
```

7. Run `npm run build`, `cdk deploy` and call api at the endpoint created with an appropriate payload.

```
npm run build
cdk deploy
curl -X POST -H 'Content-Type: application/json' -d '{"name": "Fidelis"}' https://mhi8zrb3c7.execute-api.us-east-1.amazonaws.com/prod/hello
```

Congratulations! You created your API based on an OpenAPI specification and deployed it at AWS with API Gateway and Lambda!

## Useful commands

- `npm run build` compile typescript to jsii
- `npm run watch` watch for changes and compile
- `npm run test` perform the jest unit tests
- `npm run package` package library using jsii
- `npm run coverage` run tests with coverage reports

## Developer workflow overview

![Overview](doc/images/openapi-lambda-architecture-Overview.drawio.png)

1. Developer creates an OpenAPI spec using his favorite tools
2. Developer runs OrangeStack cli or cdk init to create CDK project (when cdk init is used is necessary to import and initialize the OrangeStackOpenAPIServices construct in project stack)
3. Developer runs cdk synth to generate boilerplate code for endponts controllers and usecases implementation.
4. Developer edits/creates typescript source code generated to implement usecases
5. Develop run cdk deploy or commit the code to run CI/CD pipeline and all infrastructure needed to provide the defined services is created as a Cloudformation Stack in AWS Account.

## Generated source code

![Overview](doc/images/openapi-lambda-architecture-ComponentsView.drawio.png)

The generated source code is organized in a layered archicture with some basic components: **Core components, API Schemas and Errors, Parameters Configurations, Operation Controllers and Operation Use Cases.**

### Core components (src/core/\*\*/\*.ts)

- Core components are responsible to provide utilities and base classes for other components.

- You can create you own files in core components but the files created by the CDK Construction are overwrited when a cdk command is runned by the user. **DON'T** change the core generated files or your changes **WILL BE LOST**.

- You can customize the base components like Controller extending them and adding/changing behaviours as you need.

- The base classes provides some template methods to customize their behaviour in subclasses.

### API schemas (src/api-schemas.ts and src/error.ts)

- All schemas defined in [OpenAPI components section](https://swagger.io/specification/#components-object) are parsed and represented as typescript interfaces in `src/api-schemas.ts.

- All changes made to the schemas are reflected to this file when use execute cdk synth and the file is overwrited, so **DON'T** change this file or your changes **WILL BE LOST**.

- The construct also create an error structure that is used as base class for errors in api.

- All responses of api operations that are not 2xx response codes generates an Error subclass representing this return code. When you need to return this response to user you can throw the corresponding error and the controller will convert the response accordingly.

### Parameters configuration (src/\<operationId\>/parameters-configuration.json)

- All operations defined in [OpenAPI operations objects](https://swagger.io/specification/#operation-object) generates a JSON file with the parameters defined by the operation.

- The operation controller uses this configuration to know how to parse the parameters from API Gateway event and convert them to API schema objects and parameters to operation use case responsible to process the request.

- Parameters configurations are overwrited every time the user execute a cdk command so **DON'T** change this file or your changes **WILL BE LOST**.

- There are some known limitations in parameters definitions in your OpenAPI spec:
  - `cookie` parameters are not supported.
  - `arrays` are not supported in path or header parameters.
  - parameters can be only primitive types `object` parameters are not supported.
  - `style` an `explode` parameters modifiers are not supported.
  - parameters cannot be `arrays` of `arrays`.

### Operation controllers (src/\<operationId\>/controller.ts)

- Controllers are responsible to convert API Gateway event to parameters and API schemas representations a to execute use cases with the parameters already converted.

- They also can be responsible to validate resource access using an use case and throwing AccessDeniedError when necessary.

- Controllers have some template methods that can be use to customize their behaviour:

  - `buildUseCaseArgs` can be ovewrited to customize use case arguments when needed.
  - `transformUseCaseResponse` can be overwrited to transform use case response before converting it to success response.
  - `convertResponseToApiGatewayResponse` can be overwrited to customize the response creation. A common usecase is to return binary data instead of JSON.

- Controllers are generated when user executes a cdk command but don't ovewrite already generated controllers, so you can safelly modify controllers source code as you need.

### Operation use cases (src/\<operationId\>/usecase.ts)

- Use cases are responsible to implement the business rules of API.

- We recomend to isolate database interactions in repositories to abstract database acesss.

- Error codes represented by operation responses are generated as error classes in usecase file and this errors can be throw to generate the non success response of API.

- The requestBody when exists is converted by controllers and received as use case parameter.

- When JWT authentication is enabled `jwtTokenPayload` is passed as parameter for use case so they can do security assertions.

- Use cases are generated when user executes a cdk command but don't ovewrite already generated use cases, so you can safelly modify use case source code as you need.

## Generated Infrastucture

![infrastructure](doc/images/openapi-lambda-architecture-InfrastructureView.drawio.png)

- The CDK construct generates the following components in the stack:
  - API gateway with all operations defined in spec
  - One lambda per operation defined
  - Cloudwatch logs of lambdas
  - Enables X-Ray for api gateway and generated lambdas
  - Adjust lambda permissions to be invoked by api gateway

## Enabling JWT Security

- The construct supports JWT tokens for security and the security can be enabled defining the following [OpenAPI security scheme](https://swagger.io/specification/#security-scheme-object):

```yaml
components:
  securitySchemes:
    jwtAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
```

- You can enable JWT token validation for all operations defining a securty constraint at api root level or at operation level as shown below:

```yaml
# root level
security:
  - jwtAuth: []

paths:
  /auctions/{id}:
    parameters:
      - $ref: '#/components/parameters/AuctionId'
      - $ref: '#/components/parameters/Authorization'
    get:
      operationId: get-auction
      description: Get auction data by id

      # operation level
      security:
        - jwtAuth: []

      tags:
        - Auction services
      responses:
        '200':
          description: Auction returned successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Auction'
        '404':
          description: Auction not found
```

- Most IDM providers expose a JWKS_URI with their public keys to verify JWT token signatures. You need to configure the construct as shown below to inform JWKS_URI to be used to get the public keys:

```typescript
const api = new OrangeStackOpenApiServices(this, 'FidelisAPI', {
  specPath: 'spec/auction-api.yaml',
  jwksUri: 'https://some.idm.provider/auth/realms/some-realm/protocol/openid-connect/certs',
});
```

- If you are using an IDM that supports OpenID connect you can get JWKS_URI endpoint in [well-known endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) of OpenID connect provider.

- When you enable JWT authorization your controllers will extend `JWTAuthorizationController` class and you can override the `authorizeResourceAccess` method to do some custom authorization logic. The JWT token payload can be accessed using `this.jwtTokenPayload` protected property. Controlles already generated before JWT authorization will not be overwrited an must be changed by the user.

- With JWT Authorization enabled an API Gateway Lambda authorizer will be configured to validate the token.

- Authorization logic is not made by this lambda only the authenticity and validity of token is verified. You need to implement your authorization logic using token claims in operations controllers or create a new base constroller class based on `JWTAuthorizationControler` to use as base class of your controllers and implement authorization logic on it.

## References

### CDK

- <https://docs.aws.amazon.com/cdk/latest/guide/home.html> - CDK Developer guide
- <https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigateway-readme.html> - CDK Amazon API Gateway Construct Library
- <https://aws.github.io/jsii/user-guides/lib-author/> - jsii library author guide
- <https://cdk-advanced.workshop.aws/> - CDK advanced workshop
- <https://docs.aws.amazon.com/cdk/latest/guide/videos.html> - CDK Videos

### API Gateway

- <https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html> - API Gateway developer guide
- <https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-request-validation-set-up.html#api-gateway-request-validation-setup-importing-swagger> - How to setup API Gateway validation with OpenAPI extensions
- <https://stackoverflow.com/questions/47953570/get-detailed-error-messages-from-aws-api-gateway-request-validator> - How to get detailed body request validation messages in API Gateway

### OpenAPI

- <https://swagger.io/specification/> - OpenAPI 3.0 specification
- <https://stoplight.io/studio/> - StopLight Studio for visual specification editor and other cool features