How to use OpenAPI with API Gateway REST APIs
Initialize a complex API with a single document
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:
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:
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:
And also associates the model with the method's body:
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 $ref
s 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:
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:
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.