AWS Compute Blog

Using API Gateway mapping templates to handle changes in your back-end APIs

by Stefano Buliani | on | in Amazon API Gateway | | Comments

Maitreya Ranganath Maitreya Ranganath, AWS Solutions Architect

Changes to APIs are always risky, especially if changes are made in ways that are not backward compatible. In this blog post, we show you how to use Amazon API Gateway mapping templates to isolate your API consumers from API changes. This enables your API consumers to migrate to new API versions on their own schedule.

For an example scenario, we start with a very simple Store Front API with one resource for orders and one GET method. For this example, the API target is implemented in AWS Lambda to keep things simple – but you can of course imagine the back end being your own endpoint.

The structure of the API V1 is:

Method:		GET
Path:		/orders
Query Parameters:
	start = timestamp
	end = timestamp

Response:
[
  {
    “orderId” : string,
    “orderTs” : string,
    “orderAmount” : number
  }
]

The initial version (V1) of the API was implemented when there were few orders per day. The API was not paginated; if the number of orders that match the query is larger than 5, an error returns. The API consumer must then submit a request with a smaller time range.

The API V1 is exposed through API Gateway and you have several consumers of this API in Production.

After you upgrade the back end, the API developers make a change to support pagination. This makes the API more scalable and allows the API consumers to handle large lists of orders by paging through them with a token. This is a good design change but it breaks backward compatibility. It introduces a challenge because you have a large base of API consumers using V1 and their code can’t handle the changed nesting structure of this response.

The structure of API V2 is:

Method:		GET
Path:		/orders
Query Parameters:
	start =	timestamp
	end =	timestamp
	token =	string (optional)

Response:
{
  “nextToken” : string,
  “orders” : [
    {
      “orderId” : string,
      “orderTs” :  string
      “orderAmount” : number
    }
  ]
}

Using mapping templates, you can isolate your API consumers from this change: your existing V1 API consumers will not be impacted when you publish V2 of the API in parallel. You want to let your consumers migrate to V2 on their own schedule.

We’ll show you how to do that in this blog post. Let’s get started.

Deploying V1 of the API

To deploy V1 of the API, create a simple Lambda function and expose that through API Gateway:

  1. Sign in to the AWS Lambda console.
  2. Choose Create a Lambda function.
  3. In Step 1: Select blueprint, choose Skip; you’ll enter the details for the Lambda function manually.
  4. In Step 2: Configure function, use the following values:
    • In Name, type getOrders.
    • In Description, type Returns orders for a time-range.
    • In Runtime, choose Node.js.
    • For Code entry type, choose Edit code inline. Copy and paste the code snippet below into the code input box.
MILISECONDS_DAY = 3600*1000*24;

exports.handler = function(event, context) {
    console.log('start =', event.start);
    console.log('end =', event.end);
    
    start = Date.parse(decodeURIComponent(event.start));
    end = Date.parse(decodeURIComponent(event.end));
    
    if(isNaN(start)) {
        context.fail("Invalid parameter 'start'");
    }
    if(isNaN(end)) {
        context.fail("Invalid parameter 'end'");
    }

    duration = end - start;
    
    if(duration  5 * MILISECONDS_DAY) {
        context.fail("Too many results, try your request with a shorter duration");
    }
    
    orderList = [];
    count = 0;
    
    for(d = start; d < end; d += MILISECONDS_DAY) {
        order = {
            "orderId" : "order-" + count,
            "orderTs" : (new Date(d).toISOString()),
            "orderAmount" : Math.round(Math.random()*100.0)
        };
        count += 1;
        orderList.push(order);
    }
    
    console.log('Generated', count, 'orders');
    context.succeed(orderList);
};
    • In Handler, leave the default value of index.handler.
    • In Role, choose Basic execution role or choose an existing role if you’ve created one for Lambda before.
    • In Advanced settings, leave the default values and choose Next.

Finally, review the settings in the next page and choose Create function.

Your Lambda function is now created. You can test it by sending a test event. Enter the following for your test event:

{
  "start": "2015-10-01T00:00:00Z",
  "end": "2015-10-04T00:00:00Z"
}

Check the execution result and log output to see the results of your test.

Next, choose the API endpoints tab and then choose Add API endpoint. In Add API endpoint, use the following values:

  • In API endpoint type, choose API Gateway
  • In API name, type StoreFront
  • In Resource name, type /orders
  • In Method, choose GET
  • In Deployment stage, use the default value of prod
  • In Security, choose Open to allow the API to be publicly accessed
  • Choose Submit to create the API

The API is created and the API endpoint URL is displayed for the Lambda function.

Next, switch to the API Gateway console and verify that the new API appears on the list of APIs. Choose StoreFront to view its details.

To view the method execution details, in the Resources pane, choose GET. Choose Integration Request to edit the method properties.

On the Integration Request details page, expand the Mapping Templates section and choose Add mapping template. In Content-Type, type application/json and choose the check mark to accept.

Choose the edit icon to the right of Input passthrough. From the drop down, choose Mapping template and copy and paste the mapping template text below into the Template input box. Choose the check mark to create the template.

{
#set($queryMap = $input.params().querystring)

#foreach( $key in $queryMap.keySet())
  "$key" : "$queryMap.get($key)"
  #if($foreach.hasNext),#end
#end
}

This step is needed because the Lambda function requires its input as a JSON document. The mapping template takes query string parameters from the GET request and creates a JSON input document. Mapping templates use Apache Velocity, expose a number of utility functions, and give you access to all of the incoming requests data and context parameters. You can learn more from the mapping template reference page.

Back to the GET method configuration page, in the left pane, choose the GET method and then open the Method Request settings. Expand the URL Query String Parameters section and choose Add query string. In Name, type start and choose the check mark to accept. Repeat the process to create a second parameter named end.

From the GET method configuration page, in the top left, choose Test to test your API. Type the following values for the query string parameters and then choose Test:

  • In start, type 2015-10-01T00:00:00Z
  • In end, type 2015-10-04T00:00:00Z

Verify that the response status is 200 and the response body contains a JSON response with 3 orders.

Now that your test is successful, you can deploy your changes to the production stage. In the Resources pane, choose Deploy API. In Deployment stage, choose prod. In Deployment description, type a description of the deployment, and then choose Deploy.

The prod Stage Editor page appears, displaying the Invoke URL. In the CloudWatch Settings section, choose Enable CloudWatch Logs so you can see logs and metrics from this stage. Keep in mind that CloudWatch logs are charged to your account separately from API Gateway.

You have now deployed an API that is backed by V1 of the Lambda function.

Testing V1 of the API

Now you’ll test V1 of the API with curl and confirm its behavior. First, copy the Invoke URL and add the query parameters ?start=2015-10-01T00:00:00Z&end=2015-10-04T00:00:00Z and make a GET invocation using curl.

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-04T00:00:00Z" 

[
  {
    "orderId": "order-0",
    "orderTs": "2015-10-01T00:00:00.000Z",
    "orderAmount": 82
  },
  {
    "orderId": "order-1",
    "orderTs": "2015-10-02T00:00:00.000Z",
    "orderAmount": 3
  },
  {
    "orderId": "order-2",
    "orderTs": "2015-10-03T00:00:00.000Z",
    "orderAmount": 75
  }
]

This should output a JSON response with 3 orders. Next, check what happens if you use a longer time-range by changing the end timestamp to 2015-10-15T00:00:00Z:

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-15T00:00:00Z"
 
{
  "errorMessage": "Too many results, try your request with a shorter duration"
}

You see that the API returns an error indicating the time range is too long. This is correct V1 API behavior, so you are all set.

Updating the Lambda Function to V2

Next, you will update the Lambda function code to V2. This simulates the scenario of the back end of your API changing in a manner that is not backward compatible.

Switch to the Lambda console and choose the getOrders function. In the code input box, copy and paste the code snippet below. Be sure to replace all of the existing V1 code with V2 code.

MILISECONDS_DAY = 3600*1000*24;

exports.handler = function(event, context) {
    console.log('start =', event.start);
    console.log('end =', event.end);
    
    start = Date.parse(decodeURIComponent(event.start));
    end = Date.parse(decodeURIComponent(event.end));
    
    token = NaN;
    if(event.token) {
        s = new Buffer(event.token, 'base64').toString();
        token = Date.parse(s);
    }
    

    if(isNaN(start)) {
        context.fail("Invalid parameter 'start'");
    }
    if(isNaN(end)) {
        context.fail("Invalid parameter 'end'");
    }
    if(!isNaN(token)) {
        start = token;
    }

    duration = end - start;
    
    if(duration <= 0) {
        context.fail("Invalid parameters 'end' must be greater than 'start'");
    }
    
    orderList = [];
    count = 0;
    
    console.log('start=', start, ' end=', end);
    
    for(d = start; d < end && count < 5; d += MILISECONDS_DAY) {
        order = {
            "orderId" : "order-" + count,
            "orderTs" : (new Date(d).toISOString()),
            "orderAmount" : Math.round(Math.random()*100.0)
        };
        count += 1;
        orderList.push(order);
    }

    nextToken = null;
    if(d < end) {
        nextToken = new Buffer(new Date(d).toISOString()).toString('base64');
    }
    
    console.log('Generated', count, 'orders');

    result = {
        orders : orderList,
    };

    if(nextToken) {
        result.nextToken = nextToken;
    }
    context.succeed(result);
};

Choose Save to save V2 of the code. Then choose Test. Note that the output structure is different in V2 and there is a second level of nesting in the JSON document. This represents the updated V2 output structure that is different from V1.

Next, repeat the curl tests from the previous section. First, do a request for a short time duration. Notice that the response structure is nested differently from V1 and this is a problem for our API consumers that expect V1 responses.

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-04T00:00:00Z" 

{
  "orders": [
    {
      "orderId": "order-0",
      "orderTs": "2015-10-01T00:00:00.000Z",
      "orderAmount": 8
    },
    {
      "orderId": "order-1",
      "orderTs": "2015-10-02T00:00:00.000Z",
      "orderAmount": 92
    },
    {
      "orderId": "order-2",
      "orderTs": "2015-10-03T00:00:00.000Z",
      "orderAmount": 84
    }
  ]
}

Now, repeat the request for a longer time range and you’ll see that instead of an error message, you now get the first page of information with 5 orders and a nextToken that will let you request the next page. This is the paginated behavior of V2 of the API.

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-15T00:00:00Z"

{
  "orders": [
    {
      "orderId": "order-0",
      "orderTs": "2015-10-01T00:00:00.000Z",
      "orderAmount": 62
    },
    {
      "orderId": "order-1",
      "orderTs": "2015-10-02T00:00:00.000Z",
      "orderAmount": 59
    },
    {
      "orderId": "order-2",
      "orderTs": "2015-10-03T00:00:00.000Z",
      "orderAmount": 21
    },
    {
      "orderId": "order-3",
      "orderTs": "2015-10-04T00:00:00.000Z",
      "orderAmount": 95
    },
    {
      "orderId": "order-4",
      "orderTs": "2015-10-05T00:00:00.000Z",
      "orderAmount": 84
    }
  ],
  "nextToken": "MjAxNS0xMC0wNlQwMDowMDowMC4wMDBa"
}

It is clear from these tests that V2 will break the current V1 consumer’s code. Next, we show how to isolate your V1 consumers from this change using API Gateway mapping templates.

Cloning the API

Because you want both V1 and V2 of the API to be available simultaneously to your API consumers, you first clone the API to create a V2 API. You then modify the V1 API to make it behave as your V1 consumers expect.

Go back to the API Gateway console, and choose Create API. Configure the new API with the following values:

  • In API name, type StoreFrontV2
  • In Clone from API, choose StoreFront
  • In Description, type a description
  • Choose Create API to clone the StoreFront API as StoreFrontV2

Open the StoreFrontV2 API and choose the GET method of the /orders resource. Next, choose Integration Request. Choose the edit icon next to the getOrders Lambda function name.

Keep the name as getOrders and choose the check mark to accept. In the pop up, choose OK to allow the StoreFrontV2 to invoke the Lambda function.

Once you have granted API Gateway permissions to access your Lambda function, choose Deploy API. In Deployment stage, choose New stage. In Stage name, type prod, and then choose Deploy. Now you have a new StoreFrontV2 API that invokes the same Lambda function. Confirm that the API has V2 behavior by testing it with curl. Use the Invoke URL for the StoreFrontV2 API instead of the previously used Invoke URL.

Update the V1 of the API

Now you will use mapping templates to update the original StoreFront API to preserve V1 behavior. This enables existing consumers to continue to consume the API without having to make any changes to their code.

Navigate to the API Gateway console, choose the StoreFront API and open the GET method of the /orders resource. On the Method Execution details page, choose Integration Response.

Expand the default response mapping (HTTP status 200), and expand the Mapping Templates section. Choose Add Mapping Template.

In Content-type, type application/json and choose the check mark to accept. Choose the edit icon next to Output passthrough to edit the mapping templates. Select Mapping template from the drop down and copy and paste the mapping template below into the Template input box.

#set($nextToken = $input.path('$.nextToken'))

#if($nextToken && $nextToken.length() != 0)
  {
    "errorMessage" : "Too many results, try your request with a shorter duration"
  }
#else
  $input.json('$.orders[*]')
#end

Choose the check mark to accept and save. The mapping template transforms the V2 output from the Lambda function into the original V1 response. The mapping template also generates an error if the V2 response indicates that there are more results than can fit in one page. This emulates V1 behavior.

Finally click Save on the response mapping page. Deploy your StoreFront API and choose prod as the stage to deploy your changes.

Verify V1 behavior

Now that you have updated the original API to emulate V1 behavior, you can verify that using curl again. You will essentially repeat the tests from the earlier section. First, confirm that you have the Invoke URL for the original StoreFront API. You can always find the Invoke URL by looking at the stage details for the API.

Try a test with a short time range.

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-04T00:00:00Z"

[
  {
    "orderId": "order-0",
    "orderTs": "2015-10-01T00:00:00.000Z",
    "orderAmount": 50
  },
  {
    "orderId": "order-1",
    "orderTs": "2015-10-02T00:00:00.000Z",
    "orderAmount": 16
  },
  {
    "orderId": "order-2",
    "orderTs": "2015-10-03T00:00:00.000Z",
    "orderAmount": 14
  }
]

Try a test with a longer time range and note that the V1 behavior of returning an error is recovered.

$ curl -s "https://your-invoke-url-and-path/orders?start=2015-10-01T00:00:00Z&end=2015-10-15T00:00:00Z"

{
  "errorMessage": "Too many results, try your request with a shorter duration"
}

Congratulations, you have successfully used Amazon API Gateway mapping templates to expose both V1 and V2 versions of your API allowing your API consumers to migrate to V2 on their own schedule.

Be sure to delete the two APIs and the AWS Lambda function that you created for this walkthrough to avoid being charged for their use.