AWS APIGateway × OpenAPI (1. Motivation)

2022-07-18 (Updated on 2022-07-26)

thumbnail

I have been working on a library that integrates an OpenAPI definition with a REST API definition on the CDK. This is the first blog post of the series that will walk you through the development of the library.

Motivation

Recently, I have been urged to write the OpenAPI definitions of my REST APIs on Amazon API Gateway (API Gateway) that I have described with the Cloud Development Kit (CDK). As far as I know, there are two options to deal with the OpenAPI definition of a REST API on API Gateway.

  1. Exporting the OpenAPI definition from an existing REST API
  2. Creating a REST API by importing an existing OpenAPI definition

1. Exporting the OpenAPI definition from an existing REST API

On API Gateway, you can export the OpenAPI definition from an existing REST API. You have to add separate documentation to your REST API to export a meaningful OpenAPI definition. There is an L1 construct CfnDocumentationPart on the CDK that allows you to document an API entity; see the example in "OpenAPI and REST API side by side." However, separation of the documentation and an actual API entity definition may be disadvantageous to you to keep the documentation up-to-date. I think it would be nice if we could describe a REST API entity and its documentation side by side.

2. Creating a REST API by importing an existing OpenAPI definition

If you already have an OpenAPI definition and AWS-specific extensions in it, you can build a REST API on API Gateway by importing it. The CDK also provides a dedicated construct SpecRestApi to import an existing OpenAPI definition. However, if I build a REST API from scratch I prefer the CDK building blocks, e.g., RestApi and subsidiaries, for the following reasons,

  • Simply, I am familiar with them.
  • The CDK building blocks may reduce tedious IAM configurations and security vulnerabilities.
  • Writing AWS-specific extensions in an OpenAPI definition may be tiresome.*
  • Writing a plain OpenAPI definition may involve a lot of repetition.*

* As I have never tried this option, I might be wrong.

Third option

Thus, I want a third option that enables me to write a REST API and the OpenAPI definition side by side on top of the CDK building blocks.

Design goals

This blog post[1] introduced a similar work and inspired me. But my goal is not to utilize the power of TypeScript. I have two goals,

  1. OpenAPI and REST API side by side
  2. Compatibility with RestApi

OpenAPI and REST API side by side

With my library, we will be able to describe the OpenAPI definition beside a REST API entity rather than in a separate documentation resource.

For instance, we will be able to do

const pet = api.root.addResource('pet');
const findByStatus = pet.addResource('findByStatus');
findByStatus.addMethod(
  'GET',
  new MockIntegration({
    // ... integration settings
  }),
  {
    operationName: 'findPetsByStatus',
    summary: 'Finds Pets by status',
    description: 'Multiple status values can be provided with comma separated strings',
    requestParameters: {
      'method.request.querystring.status': {
        description: 'Status values that need to be considered to filter',
        required: false,
        explode: true,
        schema: {
          type: 'string',
          enum: ['available', 'pending', 'sold'],
          default: 'available'
        }
      }
    },
    methodResponses: [
      {
        statusCode: '200',
        description: 'successful operation',
        responseModel: {
          'application/json': petArrayModel
        }
      }
    ]
  }
);

rather than

const pet = api.root.addResource('pet');
const findByStatus = pet.addResource('findByStatus');
findByStatus.addMethod(
  'GET',
  new MockIntegration({
    // ... integration settings
  }),
  {
    operationName: 'findPetsByStatus',
    requestParameters: {
      'method.request.querystring.status': true
    },
    methodResponses: [
      {
        statusCode: '200',
        responseModel: {
          'application/json': petArrayModel
        }
      }
    ]
  }
);
new CfnDocumentationPart(this, 'FindPetsByStatusDocPart', {
  location: {
    type: 'METHOD',
    path: '/pet/findByStatus',
    method: 'GET'
  },
  properties: {
    summary: 'Finds Pets by status',
    description: 'Multiple status values can be provided with comma separated strings'
  },
  restApiId: api.restApiId
});
new CfnDocumentationPart(this, 'FindPetsByStatusParamsDocPart', {
  location: {
    type: 'QUERY_PARAMETER',
    path: '/pet/findByStatus',
    method: 'GET',
    name: 'status'
  },
  properties: {
    description: 'Status values that need to be considered to filter',
    required: false,
    explode: true,
    schema: {
        type: 'string',
        enum: ['available', 'pending', 'sold'],
        default: 'available'
    }
  },
  restApiId: api.restApiId
});
new CfnDocumentationPart(this, 'FindPetsByStatus200ResponseDocPart', {
  location: {
    type: 'RESPONSE',
    path: '/pet/findByStatus',
    method: 'GET',
    statusCode: '200'
  },
  properties: {
    description: 'successful operation'
  }
});

Compatibility with RestApi

With my library, we will be able to have an experience similar to what we have with RestApi.

All you have to do will be just instantiate an extended construct RestApiWithSpec instead of RestApi.

const api = new RestApiWithSpec(this, 'example-api', {
  description: 'Example of RestApiWithSpec',
  openApiInfo: {
    version: '1.0.0'
  },
  openApiOutputPath: 'openapi.json',
  deploy: true,
  deployOptions: {
    stageName: 'staging',
    description: 'Default stage'
  }
});

After you get an instance of RestApiWithSpec, you will be able to use it exactly like RestApi. Of course, it will also provide extensions to describe the OpenAPI definition; see the example in "OpenAPI and REST API side by side."

Challenges

There are some challenges.

  • How can we achieve compatibility with RestApi?
    • If we write a subclass or a wrapper for RestApi, we cannot directly manipulate what RestApi instantiates as subsidiaries; e.g., Resources, Methods. How can we extend the subsidiary entities?
    • If we do not reuse RestApi, it will be really tough to achieve compatibility.
  • When should the library actually output the OpenAPI definition file?
    • Should a user explicitly call a function to save?
    • Or, can we magically save the OpenAPI definition file like the CDK does to the CloudFormation template?
  • Can we support Lambda proxies?

We are going to tackle these challenges in subsequent blog posts.

Wrap-up

What I have been developing is available on my GitHub repository. In an upcoming blog post, we will tackle the challenge of how we can achieve compatibility with RestApi.

Reference

  1. OpenAPI Specs from CDK Stack WITHOUT Deploying First