The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to HTTP APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic.
In the .NET world, documenting our APIs using the OpenAPI specification is a relatively simple task, as demonstrated in the following link. How can we accomplish the same when working with AWS Lambda functions? In this article, we will attempt to answer that question, although with a little extra effort.
Pre-requisites
Install the AWS CLI.
Install the AWS SAM CLI.
The Sample Code
Download the sample code here. Open the solution and navigate to the template.yaml
file:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
SAM Template
Globals:
Function:
Timeout: 30
MemorySize: 512
Architectures:
- x86_64
Environment:
Variables:
TABLE_NAME:
Ref: DynamoTable
Resources:
DynamoTable:
Type: AWS::Serverless::SimpleTable
Properties:
PrimaryKey:
Name: petId
Type: String
TableName: petstable
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
Name: petsapi
StageName: Prod
Cors:
AllowMethods: "'*'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
RegisterFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: dotnet6
Handler: PetsApi::PetsApi.Function::Register
CodeUri: ./src/PetsApi/
Policies:
- DynamoDBCrudPolicy:
TableName:
Ref: DynamoTable
Events:
RegisterPet:
Type: Api
Properties:
Path: /pets
Method: post
RestApiId:
Ref: ApiGatewayApi
GetFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: dotnet6
Handler: PetsApi::PetsApi.Function::Get
CodeUri: ./src/PetsApi/
Policies:
- DynamoDBCrudPolicy:
TableName:
Ref: DynamoTable
Events:
GetPet:
Type: Api
Properties:
Path: /pets/{petId}
Method: get
RestApiId:
Ref: ApiGatewayApi
ListFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: dotnet6
Handler: PetsApi::PetsApi.Function::List
CodeUri: ./src/PetsApi/
Policies:
- DynamoDBCrudPolicy:
TableName:
Ref: DynamoTable
Events:
ListPet:
Type: Api
Properties:
Path: /pets
Method: get
RestApiId:
Ref: ApiGatewayApi
EditFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: dotnet6
Handler: PetsApi::PetsApi.Function::Edit
CodeUri: ./src/PetsApi/
Policies:
- DynamoDBCrudPolicy:
TableName:
Ref: DynamoTable
Events:
EditPet:
Type: Api
Properties:
Path: /pets/{petId}
Method: put
RestApiId:
Ref: ApiGatewayApi
Outputs:
PetsApi:
Description: "API Gateway endpoint URL"
Value:
Fn::Sub: "https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets/"
The application consists of four Lambda functions for registering, editing, listing, and retrieving pets from storage (DynamoDB). All the Lambda functions are accessible through an API Gateway, where we have added CORS settings to allow access from any origin (we do not recommend this for real applications). Deploy the application running sam build
and sam deploy --guided
.
OpenAPI Specification
In the solution, navigate to the openapi.yaml
file:
openapi: "3.0.1"
info:
title: "petsapi"
version: "1.0"
paths:
/pets:
get:
summary: List pets
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ArrayOfPet'
post:
summary: Register pet
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterPetRequest'
required: true
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterPetResponse'
/pets/{petId}:
get:
summary: Get pet
parameters:
- name: "petId"
in: "path"
required: true
schema:
type: "string"
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
400:
description: Not found
put:
summary: Edit pet
parameters:
- name: "petId"
in: "path"
required: true
schema:
type: "string"
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EditPetRequest'
required: true
responses:
200:
description: Successful operation
400:
description: Not found
components:
schemas:
ArrayOfPet:
type: "array"
items:
$ref: "#/components/schemas/Pet"
RegisterPetRequest:
type: object
properties:
type:
type: string
name:
type: string
required:
- type
- name
RegisterPetResponse:
type: object
properties:
petId:
type: string
required:
- petId
EditPetRequest:
type: object
properties:
type:
type: string
name:
type: string
required:
- name
- type
Pet:
type: object
properties:
petId:
type: string
type:
type: string
name:
type: string
required:
- petId
- type
- name
Navigate to https://editor.swagger.io/ and paste the content above to see a visual representation of the OpenAPI specification:
API Gateway Integration
API Gateway can be configured using an OpenAPI specification that includes a set of extensions to capture most of its specific properties, such as integration with Lambda functions. The easiest way to set a custom OpenAPI specification is by modifying the existing one that is generated by default. Run the following command to export the OpenAPI specification from the API Gateway:
aws apigateway get-export --parameters extensions='apigateway' --rest-api-id <rest_api_id>--stage-name Prod --export-type oas30 openapi_with_extensions.yaml --accepts application/yaml
The file contents should look something like this:
openapi: "3.0.1"
info:
title: "petsapi"
version: "1.0"
servers:
- url: "https://ecpgr6wvb6.execute-api.us-east-2.amazonaws.com/{basePath}"
variables:
basePath:
default: "Prod"
paths:
/pets:
get:
x-amazon-apigateway-integration:
type: "aws_proxy"
httpMethod: "POST"
uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:025381531841:function:petsapi-ListFunction-QNclQ8uvBp8f/invocations"
passthroughBehavior: "when_no_match"
post:
x-amazon-apigateway-integration:
type: "aws_proxy"
httpMethod: "POST"
uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:025381531841:function:petsapi-RegisterFunction-h7T1XcNCNd30/invocations"
passthroughBehavior: "when_no_match"
options:
responses:
"200":
description: "200 response"
headers:
Access-Control-Allow-Origin:
schema:
type: "string"
Access-Control-Allow-Methods:
schema:
type: "string"
Access-Control-Allow-Headers:
schema:
type: "string"
content: {}
x-amazon-apigateway-integration:
type: "mock"
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'*'"
method.response.header.Access-Control-Allow-Headers: "'*'"
method.response.header.Access-Control-Allow-Origin: "'*'"
responseTemplates:
application/json: "{}\n"
requestTemplates:
application/json: "{\n \"statusCode\" : 200\n}\n"
passthroughBehavior: "when_no_match"
/pets/{petId}:
get:
parameters:
- name: "petId"
in: "path"
required: true
schema:
type: "string"
x-amazon-apigateway-integration:
type: "aws_proxy"
httpMethod: "POST"
uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:025381531841:function:petsapi-GetFunction-wpx1KVbQl6fj/invocations"
passthroughBehavior: "when_no_match"
put:
parameters:
- name: "petId"
in: "path"
required: true
schema:
type: "string"
x-amazon-apigateway-integration:
type: "aws_proxy"
httpMethod: "POST"
uri: "arn:aws:apigateway:us-east-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-2:025381531841:function:petsapi-EditFunction-2iNi97wo5HYK/invocations"
passthroughBehavior: "when_no_match"
options:
parameters:
- name: "petId"
in: "path"
required: true
schema:
type: "string"
responses:
"200":
description: "200 response"
headers:
Access-Control-Allow-Origin:
schema:
type: "string"
Access-Control-Allow-Methods:
schema:
type: "string"
Access-Control-Allow-Headers:
schema:
type: "string"
content: {}
x-amazon-apigateway-integration:
type: "mock"
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'*'"
method.response.header.Access-Control-Allow-Headers: "'*'"
method.response.header.Access-Control-Allow-Origin: "'*'"
responseTemplates:
application/json: "{}\n"
requestTemplates:
application/json: "{\n \"statusCode\" : 200\n}\n"
passthroughBehavior: "when_no_match"
components: {}
The presence of the x-amazon-apigateway-integration
extension specifies the integration used for each method against the backend.
type
: The type of integration with the backend. Useaws_proxy
for integration with AWS Lambda functions.httpMethod
: The HTTP method used in the integration request. For Lambda function invocations, the value must bePOST
.uri
: The endpoint URI of the backend. For integrations of theaws_proxy
type, the value should bearn:aws:apigateway:<region_arn>:lambda:path/2015-03-31/functions/<lambda_arn>/invocations
.passthroughBehavior
: Specifies how a payload (with an unmapped content type) is passed through the integration request without any changes.
So, let's begin by replacing all the static uri
s with their corresponding values based on the Lambda functions defined in the template.yaml
file:
openapi: "3.0.1"
info:
title: "petsapi"
version: "1.0"
paths:
/pets:
get:
x-amazon-apigateway-integration:
type: "aws_proxy"
httpMethod: "POST"
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ListFunction.Arn}/invocations"
passthroughBehavior: "when_no_match"
post:
x-amazon-apigateway-integration:
type: "aws_proxy"
httpMethod: "POST"
httpMethod: "POST"
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RegisterFunction.Arn}/invocations"
passthroughBehavior: "when_no_match"
options:
responses:
"200":
description: "200 response"
headers:
Access-Control-Allow-Origin:
schema:
type: "string"
Access-Control-Allow-Methods:
schema:
type: "string"
Access-Control-Allow-Headers:
schema:
type: "string"
content: {}
x-amazon-apigateway-integration:
type: "mock"
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'*'"
method.response.header.Access-Control-Allow-Headers: "'*'"
method.response.header.Access-Control-Allow-Origin: "'*'"
responseTemplates:
application/json: "{}\n"
requestTemplates:
application/json: "{\n \"statusCode\" : 200\n}\n"
passthroughBehavior: "when_no_match"
/pets/{petId}:
get:
parameters:
- name: "petId"
in: "path"
required: true
schema:
type: "string"
x-amazon-apigateway-integration:
type: "aws_proxy"
httpMethod: "POST"
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetFunction.Arn}/invocations"
passthroughBehavior: "when_no_match"
put:
parameters:
- name: "petId"
in: "path"
required: true
schema:
type: "string"
x-amazon-apigateway-integration:
type: "aws_proxy"
httpMethod: "POST"
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EditFunction.Arn}/invocations"
passthroughBehavior: "when_no_match"
options:
parameters:
- name: "petId"
in: "path"
required: true
schema:
type: "string"
responses:
"200":
description: "200 response"
headers:
Access-Control-Allow-Origin:
schema:
type: "string"
Access-Control-Allow-Methods:
schema:
type: "string"
Access-Control-Allow-Headers:
schema:
type: "string"
content: {}
x-amazon-apigateway-integration:
type: "mock"
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'*'"
method.response.header.Access-Control-Allow-Headers: "'*'"
method.response.header.Access-Control-Allow-Origin: "'*'"
responseTemplates:
application/json: "{}\n"
requestTemplates:
application/json: "{\n \"statusCode\" : 200\n}\n"
passthroughBehavior: "when_no_match"
components: {}
The next step involves merging the content of the openapi.yaml
file with the text provided above:
openapi: "3.0.1"
info:
title: "petsapi"
version: "1.0"
paths:
/pets:
get:
summary: List pets
x-amazon-apigateway-integration:
httpMethod: "POST"
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ListFunction.Arn}/invocations"
passthroughBehavior: "when_no_match"
type: "aws_proxy"
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ArrayOfPet'
post:
summary: Register pet
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterPetRequest'
required: true
x-amazon-apigateway-integration:
httpMethod: "POST"
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RegisterFunction.Arn}/invocations"
passthroughBehavior: "when_no_match"
type: "aws_proxy"
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterPetResponse'
/pets/{petId}:
get:
summary: Get pet
parameters:
- name: "petId"
in: "path"
required: true
schema:
type: "string"
x-amazon-apigateway-integration:
httpMethod: "POST"
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetFunction.Arn}/invocations"
passthroughBehavior: "when_no_match"
type: "aws_proxy"
responses:
200:
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
400:
description: Not found
put:
summary: Edit pet
parameters:
- name: "petId"
in: "path"
required: true
schema:
type: "string"
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EditPetRequest'
required: true
x-amazon-apigateway-integration:
httpMethod: "POST"
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EditFunction.Arn}/invocations"
passthroughBehavior: "when_no_match"
type: "aws_proxy"
responses:
200:
description: Successful operation
400:
description: Not found
components:
schemas:
ArrayOfPet:
type: "array"
items:
$ref: "#/components/schemas/Pet"
RegisterPetRequest:
type: object
properties:
type:
type: string
name:
type: string
required:
- type
- name
RegisterPetResponse:
type: object
properties:
petId:
type: string
required:
- petId
EditPetRequest:
type: object
properties:
type:
type: string
name:
type: string
required:
- name
- type
Pet:
type: object
properties:
petId:
type: string
type:
type: string
name:
type: string
required:
- petId
- type
- name
The final step is to modify the ApiGatewayApi
resource in the template.yaml
file:
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
Name: petsapi
StageName: Prod
Cors:
AllowMethods: "'*'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
OpenApiVersion: '3.0.1'
DefinitionBody:
'Fn::Transform':
Name: 'AWS::Include'
Parameters:
Location: openapi_with_extensions.yaml
The Fn::Transform
and AWS::Include
directives instruct to retrieve the file from the file system and dynamically evaluate all items before inserting them into the final template that is executed.
Testing the Integration
Now that we have our specification ready run sam build
and sam deploy --guided
to deploy the application and aws apigateway get-export --parameters extensions='apigateway' --rest-api-id <rest_api_id>--stage-name Prod --export-type oas30 openapi_specification.yaml --accepts application/yaml
to download the new version of the OpenAPI specification generated by API gateway. Copy the content of the file into https://editor.swagger.io/:
Due to the CORS configuration we are using, we can test the API directly from the SwaggerUI itself. In conclusion, by following the steps presented in this article, we can effectively document your AWS Lambda functions using the OpenAPI specification, making it easier for humans and machines to understand and interact with your service. The final code can be found here. Thanks, and happy coding.