How to use OpenAPI with API Gateway REST APIs

Initialize a complex API with a single document

Author's image
Tamás Sallai
6 mins

REST API resources with OpenAPI

Similar to API Gateway HTTP APIs, REST APIs also support importing an OpenAPI document. This document is a standardized way to define APIs and various tools, such as validators and generators, can consume this format. API Gateway also supports it which gives a convenient way to set up the resources needed for the API.

To initialize a REST API using OpenAPI, choose import:

Import REST API using OpenAPI

The document defines the paths and operations:

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

The import process then creates all the API Gateway resources that are needed for the API. For example, it creates a route for the path:

Imported resources

Apart from the routes, it creates data models. For example, the operation can define a request body using a JSON schema:

paths:
  /user:
    post:
      # ...
      requestBody:
        content:
          'application/json':
            schema:
              type: object
              properties:
                name:
                  type: string
              required:
                - name
              additionalProperties: false
        required: true

The import process creates a model based on this schema:

Imported models

And also associates the model with the method's body:

Imported models is associated with the request

Vendor extensions

OpenAPI supports vendor extensions which are properties starting with x-. These properties can define configurations that are not supported by the base OpenAPI specification. API Gateway supports many such extensions for various purposes. Usually, anything that you can set using the Console can be configured embedded in the OpenAPI document.

Integration

One of the most important is how to define an integration. It specifies where and how to send the request for a given operation. This uses the x-amazon-apigateway-integration property.

Unlike HTTP APIs, REST APIs don't support using $refs for integrations, so you need to specify all its properties for every operation. Here is how to use a Lambda function to handle an operation:

paths:
  /user:
    post:
      x-amazon-apigateway-integration:
        type: aws_proxy
        uri: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/<lambda arn>/invocations
        httpMethod: POST
      # ...

The aws_proxy defines the lambda proxy integration type. It forwards everything to the function and does not transform the response besides extracting the values from the JSON object.

The uri has a specific format: arn:<partition>:apigateway:<region>:lambda:path/2015-03-31/functions/<lambda arn>/invocations. This seems complicated, but API Gateway supports variables that make it easier to construct. By using the ${AWS::Partition} and the ${AWS::Region} placeholders the only moving part is the ARN of the Lambda function.

The httpMethod is how API Gateway calls the Lambda, which is always POST.

This integration makes this flow:

Method execution flow

Request validation

REST APIs support validation both for parameters and for the request body. This is especially useful as it makes the OpenAPI document the source of truth for which requests are accepted and which are not.

You can enable validation for the whole API in one place, and all operations inherit from there. To do this, add a named validator under x-amazon-apigateway-request-validators and configure it to validate both the request body and the parameters.

To select this validator for the whole API as the default, use the x-amazon-apigateway-request-validator:

x-amazon-apigateway-request-validators:
  all:
    validateRequestBody: true
    validateRequestParameters: true
x-amazon-apigateway-request-validator: all

This setup adds the all validator to every method:

Method validator

Let's see how it works!

The API expects a request body with only a name property. When there is extra fields in the object, API Gateway returns an error without contacting the Lambda function:

--- ~ » curl -X POST -i "$API/user" -H "Content-Type: application/json" -d "{\"name\":\"testuser\",\"userid\":\"123\"}"
HTTP/2 400
content-type: application/json
content-length: 35

{"message": "Invalid request body"}

Backend

On the backend, the operationId is available in the event object under the name operationName. This makes it easy to handle multiple operations in a single function.

	const operationName = event.requestContext.operationName;
	const method = event.httpMethod;
	const body = event.body;

	if (operationName === "getUser") {
		// ...
	}else if (operationName === "updateUser" ) {
		// ...
	}

The path parameters are also extracted from the request. For example, this path has a placeholder called userid:

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

The event object contains the extracted value:

const userid = event.pathParameters.userid;

Unlike HTTP APIs, REST APIs only support the Lambda integration format 1.0. This means there is no shortcut return type, and every response must define the statusCode and stringify the body:

return {
	statusCode: 200,
	headers: {
		"Content-Type": "application/json",
	},
	body: JSON.stringify(items.Items),
};

Also, it's a bit slower than an HTTP API, as it does more things. And more importantly, it does not support the $default stage.

Terraform

To specify a Lambda function that is managed by Terraform, the OpenAPI document needs to define placeholders for the functions' ARNs:

paths:
  /user:
    post:
      x-amazon-apigateway-integration:
        type: aws_proxy
        uri: arn:$${AWS::Partition}:apigateway:$${AWS::Region}:lambda:path/2015-03-31/functions/${users_lambda_arn}/invocations
        httpMethod: POST

This construct defines the users_lambda_arn which will be substituted when Terraform deploys the stack. But since this interpolation uses the same ${...} syntax as the AWS variables, the latter needs to be escaped using $${...}.

In the tf file, the templatefile function provides a way to insert values for the placeholders:

resource "aws_api_gateway_rest_api" "rest_api" {
	name = "${random_id.id.hex}-rest-api"
	body = templatefile("api.yml", {users_lambda_arn = aws_lambda_function.users_lambda.arn})
}

During terraform apply Terraform creates the function then initializes the API using an OpenAPI document that references the Lambda function. This wires the API to the backend.

The other required resource is a deployment that creates a stage and deploys the API:

resource "aws_api_gateway_deployment" "deployment" {
	rest_api_id = aws_api_gateway_rest_api.rest_api.id
	stage_name  = "stage"
}

And finally, the permission to allow the API to call the Lambda function:

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

	source_arn = "${aws_api_gateway_rest_api.rest_api.execution_arn}/*/*/*"
}

Conclusion

The OpenAPI document provides a central place to describe various aspects of an API. API Gateway REST API supports many parts of this specification and adds its own to it. As a result, you can define all aspects of the API in the document and deploy everything in this format, letting API Gateway create the resources.

November 10, 2020
In this article