How to use OpenAPI to deploy an API Gateway HTTP API

Import and initialize an HTTP API using an OpenAPI document

Author's image
Tamás Sallai
6 mins
Photo from Wikipedia

OpenAPI with HTTP APIs

OpenAPI is a standard format that describes an API. It defines operations using paths and HTTP methods, as well as input parameters and responses for them. As its structure is well-defined, tools can use it for all sorts of purposes.

API Gateway HTTP API can consume an OpenAPI document and create the API based on its configuration. This can be done when you create the API or later to update an existing one.

Import HTTP API using OpenAPI

The OpenAPI document provides an "init document" that creates the subresources under the API. These are the same resources you need to create if you don't use the API import. Because of this, using OpenAPI does not provide any extra functionality, just a way to reuse a format you might use in other places.

Structure

An OpenAPI document defines paths and operations, such as this one that creates a user:

paths:
  /user:
    post:
      operationId: createUser
      summary: Create user
      requestBody:
        # ...
      responses:
        default:
          description: Success

When you import this definition, API Gateway creates a route at POST /user among other operations that the document defines.

HTTP API with routes from the OpenAPI document

Integrations

What is missing from the document is how to handle the request, for example, which Lambda functions to call. This is not part of the OpenAPI standard, but it allows vendor extensions that are properties starting with x-.

API Gateway supports several extensions, all start with x-amazon-apigateway-.

HTTP APIs support referencing the x-amazon-apigateway-integration using the standard $ref syntax (as opposed to REST APIs, which does not support this). This allows a Lambda integration setup to be easily shared between operations.

To define an integration, add an x-amazon-apigateway-integrations element under the components:

components:
  x-amazon-apigateway-integrations:
    users:
      type: aws_proxy
      uri: arn:aws:lambda:us-east-2:123456789012:function:my-function
      httpMethod: POST
      passthroughBehavior: when_no_match
      contentHandling: CONVERT_TO_TEXT
      payloadFormatVersion: 2.0

The above example uses the aws_proxy integration type and uses the 2.0 payload format version.

The httpMethod is how API Gateway calls the function and that has nothing to do with how users call the API. It is always POST for a Lambda function, no matter how the API was called.

To associate this integration with an operation, use the x-amazon-apigateway-integration with a reference:

paths:
  /user:
    post:
      x-amazon-apigateway-integration:
        $ref: '#/components/x-amazon-apigateway-integrations/users'
      operationId: createUser
      summary: Create user
      requestBody:
        # ...
      responses:
        default:
          description: Success

This configures an integration with the above parameters to this operation:

Integration created from the OpenAPI document

Using the global integrations object to define the targets adds just a few lines of overhead in the OpenAPI document.

Backend

When the request reaches the backend, it needs to know which operation was called. Unfortunately, the operationId is not available in the event object. If you have a separate Lambda function for each operation then it's not a problem but it's usually not the case.

If you use a separate Lambda function for each path then you can discriminate using the HTTP method.

const method = event.requestContext.http.method;
const body = event.body;

if (method === "GET") {
	// handle GET
}else if (method === "POST") {
	// handle POST
}

Otherwise, you need to check the request path.

Path parameters are supported, so at least there is no need to extract them from the request path. Path parameters are placeholders in the path:

paths:
  '/user/{userid}':
    parameters:
    - name: userid
      in: path
      required: true
      schema:
        type: string
    delete:
      operationId: deleteUser
      summary: Delete user
      responses:
        200:
          description: Success

And these are available under the pathParameters property in the event object:

const userid = event.pathParameters.userid;

Lack of validation

As importing an OpenAPI document creates the same resources as you'd create without it, it can not provide more functionality. Even though the API document specifies parameter and request body validation, API Gateway won't run them.

This means you need to implement these validations on the backend.

If you need validation on the API Gateway level you need to use REST APIs that provide this. On the other hand, a REST API requires a more complex configuration, it's a bit slower, and a bit more expensive.

Using Terraform

To wire a Lambda function that Terraform manages to an OpenAPI document, you can use the templatefile function. First, define the placeholders in the yaml:

components:
  x-amazon-apigateway-integrations:
    users:
      type: aws_proxy
      uri: ${users_lambda_arn}
      httpMethod: POST
      passthroughBehavior: when_no_match
      contentHandling: CONVERT_TO_TEXT
      payloadFormatVersion: 2.0

The uri will be whatever is passed as the users_lambda_arn. To define this variable, template the body, which is the OpenAPI document that is imported to the API:

resource "aws_apigatewayv2_api" "api" {
	name          = "api-${random_id.id.hex}"
	protocol_type = "HTTP"
	body          = templatefile("api.yml", {users_lambda_arn = aws_lambda_function.users_lambda.arn})
}

This also makes an implicit dependency between the Lambda function and the API. Terraform creates the function first, then when it's ready deploys the API.

Then you need to add a stage, which is usually taken care of in other quick-start configurations, but not when the body is used. The $default name means the stage will be available under the root.

resource "aws_apigatewayv2_stage" "stage" {
	api_id = aws_apigatewayv2_api.api.id
	name   = "$default"
	auto_deploy = true
}

Finally, you need to add the necessary permissions to the Lambda function so that the API Gateway can invoke it:

resource "aws_lambda_permission" "apigw" {
	action        = "lambda:InvokeFunction"
	function_name = aws_lambda_function.users_lambda.arn
	principal     = "apigateway.amazonaws.com"

	source_arn = "${aws_apigatewayv2_api.api.execution_arn}/*/*"
}

Conclusion

API Gateway HTTP APIs support an init document that is in standard OpenAPI format. This document defines the paths and the operations the API needs to handle and with vendor extensions, it defines how API Gateway forwards the request.

This offers a way to reuse the standard document that other tools can also use with API Gateway. But even though OpenAPI supports a feature, such as input validation or the operationId, API Gateway might not.

November 3, 2020
In this article