I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

AWS Lambda is a serverless computing service provided by Amazon Web Services.

In two previous articles, we discussed how to create an AWS Lambda function using Java, as well as how to access DynamoDB from a Lambda function.

In this tutorial, we’ll discuss how to publish a Lambda function as a REST endpoint, using AWS Gateway.

We’ll have a detailed look at the following topics:

  • Basic concepts and terms of API Gateway
  • Integration of Lambda functions with API Gateway using Lambda Proxy integration
  • Creation of an API, its structure, and how to map the API resources onto Lambda functions
  • Deployment and test of the API

2. Basics and Terms

API Gateway is a fully managed service that enables developers to create, publish, maintain, monitor, and secure APIs at any scale.

We can implement a consistent and scalable HTTP-based programming interface (also referred to as RESTful services) to access backend services like Lambda functions, further AWS services (e.g., EC2, S3, DynamoDB), and any HTTP endpoints.

Features include, but are not limited to:

  • Traffic management
  • Authorization and access control
  • Monitoring
  • API version management
  • Throttling requests to prevent attacks

Like AWS Lambda, API Gateway is automatically scaled out and is billed per API call.

Detailed information can be found in the official documentation.

2.1. Terms

API Gateway is an AWS service that supports creating, deploying, and managing a RESTful application programming interface to expose backend HTTP endpoints, AWS Lambda functions, and other AWS services.

An API Gateway API is a collection of resources and methods that can be integrated with Lambda functions, other AWS services, or HTTP endpoints in the backend. The API consists of resources that form the API structure. Each API resource can expose one or more API methods that must have unique HTTP verbs.

To publish an API, we have to create an API deployment and associate it with a so-called stage. A stage is like a snapshot in time of the API. If we redeploy an API, we can either update an existing stage or create a new one. By that, different versions of an API at the same time are possible, for example a dev stage, a test stage, and even multiple production versions, like v1, v2, etc.

Lambda Proxy integration is a simplified configuration for the integration between Lambda functions and API Gateway.

The API Gateway sends the entire request as an input to a backend Lambda function. Response-wise, API Gateway transforms the Lambda function output back to a frontend HTTP response.

3. Dependencies

We’ll need the same dependencies as in the AWS Lambda Using DynamoDB With Java article.

On top of that, we also need the JSON Simple library:

<dependency>
    <groupId>com.googlecode.json-simple</groupId>
    <artifactId>json-simple</artifactId>
    <version>1.1.1</version>
</dependency>

4. Developing and Deploying the Lambda Functions

In this section, we’ll develop and build our Lambda functions in Java, we’ll deploy it using AWS Console, and we’ll run a quick test.

As we want to demonstrate the basic capabilities of integrating API Gateway with Lambda, we’ll create two functions:

  • Function 1: receives a payload from the API, using a PUT method
  • Function 2: demonstrates how to use an HTTP path parameter or HTTP query parameter coming from the API

Implementation-wise, we’ll create one RequestHandler class, which has two methods — one for each function.

4.1. Model

Before we implement the actual request handler, let’s have a quick look at our data model:

public class Person {

    private int id;
    private String name;

    public Person(String json) {
        Gson gson = new Gson();
        Person request = gson.fromJson(json, Person.class);
        this.id = request.getId();
        this.name = request.getName();
    }

    public String toString() {
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        return gson.toJson(this);
    }

    // getters and setters
}

Our model consists of one simple Person class, which has two properties. The only notable part is the Person(String) constructor, which accepts a JSON String.

4.2. Implementation of the RequestHandler Class

Just like in the AWS Lambda With Java article, we’ll create an implementation of the RequestStreamHandler interface:

public class APIDemoHandler implements RequestStreamHandler {

    private static final String DYNAMODB_TABLE_NAME = System.getenv("TABLE_NAME"); 
    
    @Override
    public void handleRequest(
      InputStream inputStream, OutputStream outputStream, Context context)
      throws IOException {

        // implementation
    }

    public void handleGetByParam(
      InputStream inputStream, OutputStream outputStream, Context context)
      throws IOException {

        // implementation
    }
}

As we can see, the RequestStreamHander interface defines only one method, handeRequest(). Anyhow, we can define further functions in the same class, as we’ve done here. Another option would be to create one implementation of RequestStreamHander for each function.

In our specific case, we chose the former for simplicity. However, the choice must be made on a case-by-case basis, taking into consideration such factors as performance and code maintainability.

We also read the name of our DynamoDB table from the TABLE_NAME environment variable. We’ll define that variable later during deployment.

4.3. Implementation of Function 1

In our first function, we want to demonstrate how to get a payload (like from a PUT or POST request) from the API Gateway:

public void handleRequest(
  InputStream inputStream, 
  OutputStream outputStream, 
  Context context)
  throws IOException {

    JSONParser parser = new JSONParser();
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
    JSONObject responseJson = new JSONObject();

    AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
    DynamoDB dynamoDb = new DynamoDB(client);

    try {
        JSONObject event = (JSONObject) parser.parse(reader);

        if (event.get("body") != null) {
            Person person = new Person((String) event.get("body"));

            dynamoDb.getTable(DYNAMODB_TABLE_NAME)
              .putItem(new PutItemSpec().withItem(new Item().withNumber("id", person.getId())
                .withString("name", person.getName())));
        }

        JSONObject responseBody = new JSONObject();
        responseBody.put("message", "New item created");

        JSONObject headerJson = new JSONObject();
        headerJson.put("x-custom-header", "my custom header value");

        responseJson.put("statusCode", 200);
        responseJson.put("headers", headerJson);
        responseJson.put("body", responseBody.toString());

    } catch (ParseException pex) {
        responseJson.put("statusCode", 400);
        responseJson.put("exception", pex);
    }

    OutputStreamWriter writer = new OutputStreamWriter(outputStream, "UTF-8");
    writer.write(responseJson.toString());
    writer.close();
}

As discussed before, we’ll configure the API later to use Lambda proxy integration. We expect the API Gateway to pass the complete request to the Lambda function in the InputStream parameter.

All we have to do is to pick the relevant attributes from the contained JSON structure.

As we can see, the method basically consists of three steps:

  1. Fetching the body object from our input stream and creating a Person object from that
  2. Storing that Person object in a DynamoDB table
  3. Building a JSON object, which can hold several attributes, like a body for the response, custom headers, as well as an HTTP status code

One point worth mentioning here: API Gateway expects the body to be a String (for both request and response).

As we expect to get a String as body from the API Gateway, we cast the body to String and initialize our Person object:

Person person = new Person((String) event.get("body"));

API Gateway also expects the response body to be a String:

responseJson.put("body", responseBody.toString());

This topic is not mentioned explicitly in the official documentation. However, if we have a close look, we can see that the body attribute is a String in both snippets for the request as well as for the response.

The advantage should be clear: even if JSON is the format between API Gateway and the Lambda function, the actual body can contain plain text, JSON, XML, or whatever. It is then the responsibility of the Lambda function to handle the format correctly.

We’ll see how the request and response body look later when we test our functions in the AWS Console.

The same also applies to the following two functions.

4.4. Implementation of Function 2

In a second step, we want to demonstrate how to use a path parameter or a query string parameter for retrieving a Person item from the database using its ID:

public void handleGetByParam(
  InputStream inputStream, OutputStream outputStream, Context context)
  throws IOException {

    JSONParser parser = new JSONParser();
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
    JSONObject responseJson = new JSONObject();

    AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
    DynamoDB dynamoDb = new DynamoDB(client);

    Item result = null;
    try {
        JSONObject event = (JSONObject) parser.parse(reader);
        JSONObject responseBody = new JSONObject();

        if (event.get("pathParameters") != null) {
            JSONObject pps = (JSONObject) event.get("pathParameters");
            if (pps.get("id") != null) {
                int id = Integer.parseInt((String) pps.get("id"));
                result = dynamoDb.getTable(DYNAMODB_TABLE_NAME).getItem("id", id);
            }
        }
        else if (event.get("pathParameters") != null) {
            JSONObject pps = (JSONObject) event.get("pathParameters");
            if (pps.get("id") != null) {
                int id = Integer.parseInt((String) pps.get("id"));
                result = dynamoDb.getTable(DYNAMODB_TABLE_NAME).getItem("id", id);
            }
        }
        if (result != null) {
            Person person = new Person(result.toJSON());
            responseBody.put("Person", person);
            responseJson.put("statusCode", 200);
        } else {
            responseBody.put("message", "No item found");
            responseJson.put("statusCode", 404);
        }

        JSONObject headerJson = new JSONObject();
        headerJson.put("x-custom-header", "my custom header value");

        responseJson.put("headers", headerJson);
        responseJson.put("body", responseBody.toString());

    } catch (ParseException pex) {
        responseJson.put("statusCode", 400);
        responseJson.put("exception", pex);
    }

    OutputStreamWriter writer = new OutputStreamWriter(outputStream, "UTF-8");
    writer.write(responseJson.toString());
    writer.close();
}

Again, three steps are relevant:

  1. We check whether a pathParameters or an queryStringParameters array with an id attribute are present.
  2. If true, we use the belonging value to request a Person item with that ID from the database.
  3. We add a JSON representation of the received item to the response.

The official documentation provides a more detailed explanation of input format and output format for Proxy Integration.

4.5. Building Code

Again, we can simply build our code using Maven:

mvn clean package shade:shade

The JAR file will be created under the target folder.

4.6. Creating the DynamoDB Table

We can create the table as explained in AWS Lambda Using DynamoDB With Java.

Let’s choose Person as table name, id as primary key name, and Number as type of the primary key.

4.7. Deploying Code via AWS Console

After building our code and creating the table, we can now create the functions and upload the code.

This can be done by repeating steps 1-5 from the AWS Lambda with Java article, one time for each of our two methods.

Let’s use the following function names:

  • StorePersonFunction for the handleRequest method (function 1)
  • GetPersonByHTTPParamFunction for the handleGetByParam method (function 2)

We also have to define an environment variable TABLE_NAME with value “Person”.

4.8. Testing the Functions

Before continuing with the actual API Gateway part, we can run a quick test in the AWS Console, just to check that our Lambda functions are running correctly and can handle the Proxy Integration format.

Testing a Lambda function from the AWS Console works as described in AWS Lambda with Java article.

However, when we create a test event, we have to consider the special Proxy Integration format, which our functions are expecting. We can either use the API Gateway AWS Proxy template and customize that for our needs, or we can copy and paste the following events:

For the StorePersonFunction, we should use this:

{
    "body": "{\"id\": 1, \"name\": \"John Doe\"}"
}

As discussed before, the body must have the type String, even if containing a JSON structure. The reason is that the API Gateway will send its requests in the same format.

The following response should be returned:

{
    "isBase64Encoded": false,
    "headers": {
        "x-custom-header": "my custom header value"
    },
    "body": "{\"message\":\"New item created\"}",
    "statusCode": 200
}

Here, we can see that the body of our response is a String, although it contains a JSON structure.

Let’s look at the input for the GetPersonByHTTPParamFunction.

For testing the path parameter functionality, the input would look like this:

{
    "pathParameters": {
        "id": "1"
    }
}

And the input for sending a query string parameter would be:

{
    "queryStringParameters": {
        "id": "1"
    }
}

As a response, we should get the following for both cases methods:

{
  "headers": {
    "x-custom-header": "my custom header value"
  },
  "body": "{\"Person\":{\n  \"id\": 88,\n  \"name\": \"John Doe\"\n}}",
  "statusCode": 200
}

Again, the body is a String.

5. Creating and Testing the API

After we created and deployed the Lambda functions in the previous section, we can now create the actual API using the AWS Console.

Let’s look at the basic workflow:

  1. Create an API in our AWS account.
  2. Add a resource to the resources hierarchy of the API.
  3. Create one or more methods for the resource.
  4. Set up the integration between a method and the belonging Lambda function.

We’ll repeat steps 2-4 for each of our two functions in the following sections.

5.1. Creating the API

For creating the API, we’ll have to:

  1. Sign in to the API Gateway console at https://console.aws.amazon.com/apigateway
  2. Click on “Get Started” and then select “New API”
  3. Type in the name of our API (TestAPI) and acknowledge by clicking on “Create API”

Having created the API, we can now create the API structure and link it to our Lambda functions.

5.2. API Structure for Function 1

The following steps are necessary for our StorePersonFunction:

  1. Choose the parent resource item under the “Resources” tree and then select “Create Resource” from the “Actions” drop-down menu. Then, we have to do the following in the “New Child Resource” pane:
    • Type “Persons” as a name in the “Resource Name” input text field
    • Leave the default value in the “Resource Path” input text field
    • Choose “Create Resource”
  2. Choose the resource just created, choose “Create Method” from the “Actions” drop-down menu, and carry out the following steps:
    • Choose PUT from the HTTP method drop-down list and then choose the check mark icon to save the choice
    • Leave “Lambda Function” as integration type, and select the “Use Lambda Proxy integration” option
    • Choose the region from “Lambda Region”, where we deployed our Lambda functions before
    • Type “StorePersonFunction” in “Lambda Function”
  3. Choose “Save” and acknowledge with “OK” when prompted with “Add Permission to Lambda Function”

5.3. API Structure for Function 2 – Path Parameters

The steps for our retrieving path parameters are similar:

  1. Choose the /persons resource item under the “Resources” tree and then select “Create Resource” from the “Actions” drop-down menu. Then, we have to do the following in the New Child Resource pane:
    • Type “Person” as a name in the “Resource Name” input text field
    • Change the “Resource Path” input text field to “{id}”
    • Choose “Create Resource”
  2. Choose the resource just created, select “Create Method” from the “Actions” drop-down menu, and carry out the following steps:
    • Choose GET from the HTTP method drop-down list and then choose the check mark icon to save the choice
    • Leave “Lambda Function” as integration type, and select the “Use Lambda Proxy integration” option
    • Choose the region from “Lambda Region”, where we deployed our Lambda functions before
    • Type “GetPersonByHTTPParamFunction” in “Lambda Function”
  3. Choose “Save” and acknowledge with “OK” when prompted with “Add Permission to Lambda Function”

Note: it is important here to set the “Resource Path” parameter to “{id}”, as our GetPersonByPathParamFunction expects this parameter to be named exactly like this.

5.4. API Structure for Function 2 – Query String Parameters

The steps for receiving query string parameters are a bit different, as we don’t have to create a resource, but instead have to create a query parameter for the id parameter:

  1. Choose the /persons resource item under the “Resources” tree, select “Create Method” from the “Actions” drop-down menu, and carry out the following steps:
    • Choose GET from the HTTP method drop-down list and then select the checkmark icon to save the choice
    • Leave “Lambda Function” as integration type, and select the “Use Lambda Proxy integration” option
    • Choose the region from “Lambda Region”, where we deployed our Lambda functions before
    • Type “GetPersonByHTTPParamFunction” in “Lambda Function”.
  2. Choose “Save” and acknowledge with “OK” when prompted with “Add Permission to Lambda Function”
  3. Choose “Method Request” on the right and carry out the following steps:
    • Expand the URL Query String Parameters list
    • Click on “Add Query String”
    • Type “id” in the name field, and choose the check mark icon to save
    • Select the “Required” checkbox
    • Click on the pen symbol next to “Request validator” on the top of the panel, select “Validate query string parameters and headers”, and choose the check mark icon

Note: It is important to set the “Query String” parameter to “id”, as our GetPersonByHTTPParamFunction expects this parameter to be named exactly like this.

5.5. Testing the API

Our API is now ready, but it’s not public yet. Before we publish it, we want to run a quick test from the Console first.

For that, we can select the respective method to be tested in the “Resources” tree and click on the “Test” button. On the following screen, we can type in our input, as we would send it with a client via HTTP.

For StorePersonFunction, we have to type the following structure into the “Request Body” field:

{
    "id": 2,
    "name": "Jane Doe"
}

For the GetPersonByHTTPParamFunction with path parameters, we have to type 2 as a value into the “{id}” field under “Path”.

For the GetPersonByHTTPParamFunction with query string parameters, we have to type id=2 as a value into the “{persons}” field under “Query Strings”.

5.6. Deploying the API

Up to now, our API wasn’t public and thereby was only available from the AWS Console.

As discussed before, when we deploy an API, we have to associate it with a stage, which is like a snapshot in time of the API. If we redeploy an API, we can either update an existing stage or create a new one.

Let’s see how the URL scheme for our API will look:

https://{restapi-id}.execute-api.{region}.amazonaws.com/{stageName}

The following steps are required for deployment:

  1. Choose the particular API in the “APIs” navigation pane
  2. Choose “Actions” in the Resources navigation pane and select “Deploy API” from the “Actions” drop-down menu
  3. Choose “[New Stage]” from the “Deployment stage” drop-down, type “test” in “Stage name”, and optionally provide a description of the stage and deployment
  4. Trigger the deployment by choosing “Deploy”

After the last step, the console will provide the root URL of the API, for example, https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test.

5.7. Invoking the Endpoint

As the API is public now, we can call it using any HTTP client we want.

With cURL, the calls would look like as follows.

StorePersonFunction:

curl -X PUT 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons' \
  -H 'content-type: application/json' \
  -d '{"id": 3, "name": "Richard Roe"}'

GetPersonByHTTPParamFunction for path parameters:

curl -X GET 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons/3' \
  -H 'content-type: application/json'

GetPersonByHTTPParamFunction for query string parameters:

curl -X GET 'https://0skaqfgdw4.execute-api.eu-central-1.amazonaws.com/test/persons?id=3' \
  -H 'content-type: application/json'

6. Conclusion

In this article, we had a look how to make AWS Lambda functions available as REST endpoints, using AWS API Gateway.

We explored the basic concepts and terminology of API Gateway, and we learned how to integrate Lambda functions using Lambda Proxy Integration.

Finally, we saw how to create, deploy, and test an API.

As usual, all the code for this article is available over on GitHub.

I just announced the new Spring Boot 2 material, coming modules in REST With Spring:

>> CHECK OUT THE LESSONS