AWS Lambda and APIGateway as an AJAX-compatible API-endpoint with custom routing

Lambda executes your code without the need for you to manage a dedicated server. APIGateway provides a front-end for your Lambda to be easily accessed from the Internet.
A decorated hallway

AWS Lambda is a powerful tool to build serverless applications, especially when backed by APIGateway and Swagger. Lambda executes your code without the need for you to manage a dedicated server. APIGateway provides a front-end for your Lambda to be easily accessed from the Internet via endpoints that can be configured with the Swagger framework. In this article we’ll take a look at one specific example of an AJAX endpoint that uses custom path parameters, something typically problematic to implement because of Swagger limitations.


Task

Build an HTTP proxy endpoint reachable from a browser using an AJAX-call with an address like path/to/endpoint/{route+}, where route is a URL path (this means that it can contain forward slashes – e.g. path/to/endpoint/foo/bar).

Framework

AWS Lambda – provides the backend for the endpoint. It contains code with business logic, processes user input, and returns a JSON response.

Amazon APIGateway – provides the connection layer to the Lambda from the Internet over HTTP, because you can’t call Lambda functions directly without AWS credentials.

Swagger – describes the APIGateway resource configuration.

Plan

Let’s assume that you have a Lambda function already. If not, then create a new one with the following code:

exports.handler = (request, _context, callback) => {
  const body = {
    url: 'prefix/' + request.pathParameters.route
  };
  const response = {
    statusCode: 200,
    body: JSON.stringify(body)
  };
  callback(null,response);
});

This function takes a request and returns a JSON with the modified URL. In this case, the function just attaches prefix to the requested path. This is good enough for the shown example but in production you would likely have something more meaningful such as traffic-splitting, domain-switching, or other routing functionality.

In order to connect the Lambda to the Internet, we need to define a Swagger configuration for the APIGateway. You should note that the route parameter can contain any string as is the case when you build a request proxy and your route parameter contains the requested URL. Unfortunately, doing this with a basic Swagger configuration is not possible because the parameters cannot contain forward slashes. In APIGateway this would only be possible with a special extension for Swagger called x-amazon-apigateway-any-method. But herein lies the problem…

Problem

The ANY method will pass all requests to the Lambda function. It’s a good-enough solution if you call an endpoint only from a backend or mobile app but if you call it from a browser, the browser will fire a pre-flight request with the OPTIONS method. The response should contain an empty body and CORS headers which would allow us to perform the AJAX request. In this case however, the request will end up in the Lambda function and the main code will be executed, returning JSON as a result and no CORS. The browser will then reject it and throw an error.

You can hack-fix it on the side of the Lambda function by checking the request method and returning a mock for OPTIONS, but in that case, why use tools as powerful as APIGateway and Swagger in the first place?

Solution

Actually, all you need to do is to define x-amazon-apigateway-any-method with default behaviour and override the necessary methods with an actual configuration.

Here is an example:

swagger: '2.0'
info:
  title: HTTP-proxy API
  description: Example HTTP-proxy API
  version: '1.0.0'
schemes:
- https
produces:
- application/json
paths:
  /path/to/endpoint/{route+}:
    x-amazon-apigateway-any-method:
      produces:
      - application/json
      consumes:
      - application/json

      x-amazon-apigateway-integration:
        type: mock
        passthroughBehavior: when_no_templates
        responses:
          default:
            statusCode: "403"
            responseParameters:
              method.response.header.Access-Control-Allow-Origin: "'*'"
            responseTemplates:
              application/json: __passthrough__
        requestTemplates:
          application/json: "{\"statusCode\": 403}"

      responses:
        403:
          headers:
            Access-Control-Allow-Origin:
              type: string
          description: 403 response

    get:
      produces:
      - application/json
      consumes:
      - application/json

      summary: This is a test Lamda HTTP-endpoint

      parameters: &parameters
      - name: route
        in: path
        type: string
        required: true

      x-amazon-apigateway-integration:
        type: aws_proxy
        httpMethod: POST
        uri: %HERE_GOES_YOUR_LAMBDA_ARN%
        credentials: %HERE_GOES_YOUR_LAMBDA_INVOCATION_ROLE%

      responses:
        200:
          headers:
            Access-Control-Allow-Origin:
              type: string
          description: Returns the experiment configuration and the destination for a specific target

    options:
      produces:
      - application/json
      consumes:
      - application/json

      summary: OPTIONS method defined for AJAX-calls

      parameters:
      - name: route
        in: path
        type: string
        required: true

      responses:
        200:
          description: 200 response
          headers:
            Access-Control-Allow-Origin:
              type: string
            Access-Control-Allow-Methods:
              type: string
            Access-Control-Allow-Headers:
              type: string
      x-amazon-apigateway-integration:
        passthroughBehavior: when_no_templates
        responses:
          default:
            statusCode: "200"
            responseParameters:
              method.response.header.Access-Control-Allow-Methods: "'GET,POST,PUT,PATCH,DELETE,OPTIONS'"
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
            responseTemplates:
              application/json: __passthrough__
        requestTemplates:
          application/json: "{\"statusCode\": 200}"
        type: mock

Here we define a x-amazon-apigateway-any-method block that returns a 403 status code by default. After, we define a GET method, overriding the default behaviour with a call to the Lambda function. Finally, we define an OPTIONS method that returns Access-Control-Allow-* headers, necessary for AJAX calls. All we need to do now is to return the Access-Control-Allow-Origin header together with the Lambda response. Let’s modify our code:

exports.handler = (request, _context, callback) => {
  const body = {
    url: ‘prefix/’ + request.pathParameters.route
  };
  const response = {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*'
    },
    body: JSON.stringify(body)
  };
  callback(null,response);
});

Now our problem is fixed. We’ve defined an AJAX-compatible API endpoint using APIGateway tooling and can call our Lambda function from the Internet.

Photo by Jeremy Goldberg on Unsplash

Want to join our Engineering team?
Apply today!
Share: