As we discussed in our article Understanding Amazon API Gateway: Methods and Integrations, there are two types of integration with a backend service: proxy and non-proxy.
Proxy Integration
In proxy integration, Amazon API Gateway forwards the entire request to the backend service, which processes the request and returns the response. There are two types of proxy integration: Lambda Proxy Integration and HTTP Proxy Integration. The main advantages are easy configuration and control over the request and response in the backend service. However, this approach limits our ability to manipulate the request and response from the backend service.
Non-Proxy Integration
In non-proxy integration, Amazon API Gateway lets us control how requests are transformed before they are sent to the backend and how responses are returned. We explicitly configure the request and response mappings. There are two types: Lambda Custom Integration and HTTP Custom Integration. The main advantage is having control over the request and response structure, useful when integrating with backends that need specific formats. The downside is the increased complexity during configuration.
In this article, we focus on the last integration type, using a Lambda function as the backend service.
Pre-requisites
An IAM User with programmatic access
Install the AWS CLI
Install the Amazon Lambda Templates (
dotnet new -i Amazon.Lambda.Templates
)Install the Amazon Lambda Tools (
dotnet tool install -g Amazon.Lambda.Tools
)Install AWS SAM CLI
The Lambda function
Run the following commands to set up our Lambda function:
dotnet new lambda.EmptyFunction -n MyLambda -o .
dotnet add src/MyLambda package Amazon.Lambda.APIGatewayEvents
dotnet new sln -n MyApplications
dotnet sln add --in-root src/MyLambda
Open the Program.cs
file and update the content as follows:
using Amazon.Lambda.Core;
using System.Text.Json;
// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace MyLambda;
public class Function
{
public record RegisterPetRequest(string Name, string ThrowError);
public record RegisterPetResponse(Guid PetId, string Name);
public RegisterPetResponse FunctionHandler(RegisterPetRequest input, ILambdaContext context)
{
if (!string.IsNullOrEmpty(input.ThrowError))
{
throw new ApplicationException("An error was thrown");
}
return new RegisterPetResponse(Guid.NewGuid(), input.Name);
}
}
AWS SAM template
At the solution level, create a template.yml
file with the following content:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
SAM
Resources:
MyApi:
Type: AWS::ApiGateway::RestApi
Properties:
Description: API with lambda non proxy integration
Name: apilambdanonproxy
EndpointConfiguration:
Types:
- REGIONAL
MyResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId: !GetAtt MyApi.RootResourceId
PathPart: 'pets'
RestApiId: !Ref MyApi
MyApiFunction:
Type: AWS::Serverless::Function
Properties:
Timeout: 60
MemorySize: 512
Tracing: Active
Runtime: dotnet8
Architectures:
- x86_64
Handler: MyLambda::MyLambda.Function::FunctionHandler
CodeUri: ./src/MyLambda/
MyPermission:
Type: AWS::Lambda::Permission
DependsOn:
- MyApi
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref MyApiFunction
Principal: apigateway.amazonaws.com
Outputs:
MyApiEndpoint:
Description: "API endpoint"
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/prod/pets"
RestApiId:
Description: "REST API id"
Value: !Ref MyApi
Usually, the
AWS::Serverless::Function
resource simplifies setting up a Lambda function. However, in our case, it does not support non-proxy integration, so we will use CloudFormation resources to complete the setup.The
AWS::ApiGateway::RestApi
resource creates a REST API in Amazon API Gateway.The
AWS::ApiGateway::Resource
resource creates a specific URI within our REST API, which can be associated with various HTTP methods.The
AWS::Lambda::Permission
resource grants permission for an AWS service or another AWS account to invoke a specific AWS Lambda function.
The AWS::ApiGateway::Method
resource represents an HTTP method. This resource defines the settings for a particular HTTP method, such as the integration with the backend service. We will begin with the following setup:
MyMethod:
Type: AWS::ApiGateway::Method
Properties:
ResourceId: !Ref MyResource
RestApiId: !Ref MyApi
Method Request and Method Response
From the method request perspective, we add the AuthorizationType
, HttpMethod
, and RequestParameters
properties. For the method response, we define the 200
and 500
status codes and their corresponding response parameters within the MethodResponses
property. Defining the RequestParameters
and MethodResponses
is necessary to set up and understand the integration section later.
MyMethod:
Type: AWS::ApiGateway::Method
Properties:
ResourceId: !Ref MyResource
RestApiId: !Ref MyApi
AuthorizationType: NONE
HttpMethod: POST
RequestParameters:
method.request.header.version: true
MethodResponses:
- StatusCode: '200'
ResponseParameters:
method.response.header.version: true
method.response.header.id: true
- StatusCode: '500'
ResponseParameters:
method.response.header.version: true
Integration Request
The Integration property contains all the configuration for the backend service:
MyMethod:
Type: AWS::ApiGateway::Method
Properties:
ResourceId: !Ref MyResource
RestApiId: !Ref MyApi
AuthorizationType: NONE
HttpMethod: POST
RequestParameters:
method.request.header.version: true
MethodResponses:
- StatusCode: '200'
ResponseParameters:
method.response.header.version: true
method.response.header.id: true
- StatusCode: '500'
ResponseParameters:
method.response.header.version: true
Integration:
IntegrationHttpMethod: POST
Type: AWS
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyApiFunction.Arn}/invocations"
The
IntegrationHttpMethod
property specifies the HTTP method used to invoke the backend service.Valid values for the
Type
property areAWS
,AWS_PROXY
,HTTP
,HTTP_PROXY
, andMOCK
.For
HTTP
andHTTP_PROXY
types, theUri
property holds the HTTP URL of our backend service. ForAWS
orAWS_PROXY
types, the value follows the formatarn:aws:apigateway:{region}:{subdomain.service|service}:path|action/{service_api}
.
Request Mapping
Body
To map the body from the method request to the backend service, we use the RequestTemplates
property. The property's value is a dictionary where the content type is the key and the template, written in Velocity Template Language (VTL), is the content. The following list shows the variables we can use during the mapping:
The
$input
variable lets us access the body, query strings, headers, and path variables.The
$context
variable gives information about the Amazon API Gateway execution context.The
$stageVariable
can be used to inject environment values from the stage.The
$util
variable contains utility functions.
MyMethod:
Type: AWS::ApiGateway::Method
Properties:
ResourceId: !Ref MyResource
RestApiId: !Ref MyApi
AuthorizationType: NONE
HttpMethod: POST
RequestParameters:
method.request.header.version: true
MethodResponses:
- StatusCode: '200'
ResponseParameters:
method.response.header.version: true
method.response.header.id: true
- StatusCode: '500'
ResponseParameters:
method.response.header.version: true
Integration:
PassthroughBehavior: WHEN_NO_TEMPLATES
IntegrationHttpMethod: POST
Type: AWS
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyApiFunction.Arn}/invocations"
RequestTemplates:
"application/json": "
#set($inputRoot = $input.path('$'))
{
\"name\": \"$inputRoot.name\",
\"throwError\":\"$input.params('throwerror')\"
}"
The PassthroughBehavior
property determines how the body of an unmapped content type will be sent to the backend service:
WHEN_NO_MATCH
: The request is passed to the backend without transformation when the content type does not match any content types associated with the mapping templates.NEVER
: The request is rejected if no template matches.WHEN_NO_TEMPLATES
: The request is passed to the backend without transformation only when no mapping template is defined in the integration request.
We can use https://velocity-template-tester.onrender.com/ to test our templates.
Headers
To map headers from the method request to the backend service, we use the RequestParameters
property. The headers must also be defined in the method request.
MyMethod:
Type: AWS::ApiGateway::Method
Properties:
ResourceId: !Ref MyResource
RestApiId: !Ref MyApi
AuthorizationType: NONE
HttpMethod: POST
RequestParameters:
method.request.header.version: true
MethodResponses:
- StatusCode: '200'
ResponseParameters:
method.response.header.version: true
method.response.header.id: true
- StatusCode : '500'
ResponseParameters:
method.response.header.version: true
Integration:
PassthroughBehavior: WHEN_NO_TEMPLATES
IntegrationHttpMethod: POST
Type: AWS
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyApiFunction.Arn}/invocations"
RequestTemplates:
"application/json": "
#set($inputRoot = $input.path('$'))
{
\"name\": \"$inputRoot.name\",
\"throwError\":\"$input.params('throwerror')\"
}"
RequestParameters:
integration.request.header.version: method.request.header.version
The headers can come from the method request or static values. You can find more information here.
Integration Response
Now, let's check the IntegrationResponses
property, which specifies how to handle the response from the backend service. We can define multiple records under this property, each identified by the StatusCode
property. These status codes must match the ones defined in the method response section. Every item has ResponseTemplates
and ResponseParameters
properties to map the body and headers, using the same rules we discussed earlier.
The SelectionPattern
property helps us match an integration response set up with a specific response from the backend service. An empty value is considered the default mapping and will be evaluated last. When the backend service is an HTTP endpoint or AWS service, this pattern is evaluated against the response status code. An exception is the Lambda functions, where error messages always appear in the errorMessage
field in the response, so we match against that.
MyMethod:
Type: AWS::ApiGateway::Method
Properties:
ResourceId: !Ref MyResource
RestApiId: !Ref MyApi
AuthorizationType: NONE
HttpMethod: POST
RequestParameters:
method.request.header.version: true
MethodResponses:
- StatusCode: '200'
ResponseParameters:
method.response.header.version: true
method.response.header.id: true
- StatusCode : '500'
ResponseParameters:
method.response.header.version: true
Integration:
PassthroughBehavior: WHEN_NO_TEMPLATES
IntegrationHttpMethod: POST
Type: AWS
Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyApiFunction.Arn}/invocations"
RequestTemplates:
"application/json": "
#set($inputRoot = $input.path('$'))
{
\"name\": \"$inputRoot.name\",
\"throwError\":\"$input.params('throwerror')\"
}"
RequestParameters:
integration.request.header.version: method.request.header.version
IntegrationResponses:
- StatusCode: '200'
ResponseTemplates:
"application/json": "
#set($inputRoot = $input.path('$'))
{
\"petId\": \"$inputRoot.PetId\"
}"
ResponseParameters:
method.response.header.version: "'v1'"
method.response.header.id: integration.response.body.PetId
- StatusCode: '500'
SelectionPattern: ".*An error was thrown.*"
ResponseTemplates:
"application/json": "
#set($inputRoot = $input.path('$'))
{
\"error\": \"$inputRoot.errorType\"
}"
ResponseParameters:
method.response.header.version: "'v1'"
Run the following commands to deploy the resources to AWS:
sam build
sam deploy --guided
aws apigateway create-deployment --rest-api-id [RestApiId] --stage-name prod --description "my stage"
You can find the final code here. Thanks, and happy coding.