How to develop AWS Lambda Serverless CRUD API With Java

How to develop AWS Lambda Serverless CRUD API With Java

AWS Lambda is Amazon Web Services (AWS) serverless computing platform that allows developers to run their code without building or managing servers. Java is one of the most popular programming languages ​​supported by AWS Lambda. With the ability to use Java, developers can use their existing skills and libraries to build and deploy serverless applications on the AWS Lambda platform.

Java developers can take advantage of the flexibility and scalability of AWS Lambda by creating and running Java-based AWS Lambda functions. This process involves setting up the required environment, writing and testing the code, and deploying operations on the AWS Lambda platform. Using Java on AWS Lambda, developers can quickly build and deploy serverless applications that handle any traffic.

In this article, we will explain how we can build CRUD API with the following tech stack in a practical scenario,

topics that we are going to cover,

Requirement For REST API Development

In this article, we will develop serverless functions to support the following requirements for storing author data sets on DynamoDB tables with covering CRUD API functions.

API PATHHTTP METHODDESCRIPTION
/authorsGETRead authors registered in the DB, Here this API should support both findAll and findById functions in single API path.
/authorsPOSTCreate author data on dynamoDB database.
/authors/:idPATCHDelete author data under the given ID.
/authors/:idDELETERead authors registered in the DB, Here this API should support both findAll and findById functions in a single API path.

Building Base Serverless Application

In this tutorial, we are going to use serverless CLI to create the application and manage the API infrastructure in later stages.

Creating a serverless application with Java 8 runtime

For the moment, serverless supports the following templates,

  • aws-java-gradle
  • aws-java-maven

Choose a maven or gradle java template as you prefer. We are going to use gradle based java application.

$ serverless create --template aws-java-gradle --path aws-lambda-serverless-crud-java

Basic project structure

AWS Lambda Serverless application basic structure

  • com.serverless package - Here we are keeping all the sources and this is act as the base package for the project.

  • serverless.yml - With this, we can configure the serverless application infrastructure, API paths, Resources, environment variables, permissions and etc. Basically, serverless CLI uses this yml file to set up the cloud formation instructions for this application.

  • Handler - We can create handlers and point those from serverless.yml with an API endpoint (Optional). Basically, the handler acts as the starting point for the serverless API.

Configure distributions and other configs for the deployment - Optional step

By default the serverless application creates the distribution on build/distributions under hello.zip name.

We can change this and create and name the final distribution the way we like.

change the build.gradle as below under buildZip task.

task buildZip(type: Zip) {
    archiveBaseName = "aws-lambda-serverless-crud-java"
    from compileJava
    from processResources
    into('lib') {
        from configurations.runtimeClasspath
    }
}

Then change the distribution path in the serverless.yml,

package:
  artifact: build/distributions/aws-lambda-serverless-crud-java.zip

Also, we can configure the region and application stage we are going to deploy this application. Feel free to create your application with any region and stage.

provider:
  name: aws
  runtime: java8
  stage: development
  region: us-west-2

DynamoDB tables and permission to access from serverless

In this requirement, we have 2 ways to configure the database setup and create permissions which are a manual way and an automated way we can easily setup using serverless.yml

Let’s focus on the automated way we can configure via serverless.yml,

First, we need to create the DynamoDB table we need and add permissions to access the table from this serverless application. Here we are using an environment variable to set the database table, because it will make our life easier when we access the database at later stages.

provider:
  name: aws
  runtime: java8
  stage: development
  region: us-west-2

  environment:
    REGION: ${opt:region, self:provider.region}
    AUTHOR_TABLE: javatodev-author-${opt:stage, self:provider.stage}

Then the table definition,

resources:
  Resources:
    AuthorDynamoDBTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: "id"
            AttributeType: "S"
        KeySchema:
          - AttributeName: "id"
            KeyType: "HASH"
        StreamSpecification:
          StreamViewType: "NEW_AND_OLD_IMAGES"
        TableName: ${self:provider.environment.AUTHOR_TABLE}

Now we have created the definitions to create the necessary dynamo DB tables on this application.

Then we need to give the necessary permissions to use these tables on application API processes.

provider:
  name: aws
  runtime: java8
  stage: development
  region: us-west-2

  # ENVIRONMENT VARIABLES
  environment:
    REGION: ${opt:region, self:provider.region}
    AUTHOR_TABLE: javatodev-author-${opt:stage, self:provider.stage}

  # IAM ROLES TO ACCESS DYNAMODB TABLES
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:BatchGetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - !GetAtt AuthorDynamoDBTable.Arn

Here we need to show the correct ARN to identify our table which was created with the application. We can point it using !GetAtt AuthorDynamoDBTable.Arn.

API Development

Now we can focus on developing API handlers and exposing those via HTTP API with AWS lambda serverless.

DTO / Utils and Other Classes

In this API, we are using a separate Util class which converts incoming request body string to java POJO using jackson ObjectMapper.

package com.serverless.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.serverless.exception.IncomingRequestParsingException;

public class RequestConversionUtil {

    ObjectMapper objectMapper = new ObjectMapper();

    public <T> T parseRequestBody(String requestBodyContent, Class<T> outPutClass) {
        try {
            return objectMapper.readValue(requestBodyContent, outPutClass);
        } catch (JsonProcessingException e) {
            throw new IncomingRequestParsingException();
        }
    }

}

Also, there are 2 main model classes we are using to bring data in and out from the API.

package com.serverless.model;

public class AuthorDto {

    private String id;
    private String firstName;
    private String lastName;
    private String email;
    private String identificationNumber;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getIdentificationNumber() {
        return identificationNumber;
    }

    public void setIdentificationNumber(String identificationNumber) {
        this.identificationNumber = identificationNumber;
    }

    @Override public String toString() {
        return "AuthorDto{" +
            "id='" + id + '\'' +
            ", firstName='" + firstName + '\'' +
            ", lastName='" + lastName + '\'' +
            ", email='" + email + '\'' +
            ", identificationNumber='" + identificationNumber + '\'' +
            '}';
    }
}
package com.serverless.model;

public class CommonAPIResponse {
    private String message;

    public CommonAPIResponse(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

Base API Endpoint - GET

Just a simple JSON response with an incoming request.

functions:
  base_api:
    handler: com.serverless.Handler
    events:
      - httpApi:
          path: /
          method: get
package com.serverless;

import java.util.Collections;
import java.util.Map;

import org.apache.log4j.Logger;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class Handler implements RequestHandler<Map<String, Object>, ApiGatewayResponse> {

	private static final Logger LOG = Logger.getLogger(Handler.class);

	@Override
	public ApiGatewayResponse handleRequest(Map<String, Object> input, Context context) {
		Response responseBody = new Response("Go Serverless v1.x! Your function executed successfully!", input);
		return ApiGatewayResponse.builder()
				.setStatusCode(200)
				.setObjectBody(responseBody)
				.setHeaders(Collections.singletonMap("Content-Type", "application/json"))
				.build();
	}
}

Author Creation API Endpoint - POST

Here we are going to create an API endpoint that supports a POST HTTP method that allows us to bring requestBody with an incoming request.

  author_registration:
    handler: com.serverless.author.RegistrationHandler
    events:
      - httpApi:
          path: /authors/registration
          method: post
package com.serverless.author;

import com.amazonaws.regions.Regions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.serverless.ApiGatewayResponse;
import com.serverless.Handler;
import com.serverless.model.AuthorDto;
import com.serverless.model.CommonAPIResponse;
import com.serverless.util.RequestConversionUtil;

import org.apache.log4j.Logger;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class RegistrationHandler implements RequestHandler<Map<String, Object>, ApiGatewayResponse> {

    private static final Logger LOG = Logger.getLogger(RegistrationHandler.class);
    private AmazonDynamoDB amazonDynamoDB;
    private String AUTHOR_DB_TABLE = System.getenv("AUTHOR_TABLE");
    private Regions REGION = Regions.fromName(System.getenv("REGION"));

    @Override public ApiGatewayResponse handleRequest(Map<String, Object> input, Context context) {
        RequestConversionUtil requestConversionUtil = new RequestConversionUtil();
        AuthorDto request = requestConversionUtil.parseRequestBody(input.get("body").toString(), AuthorDto.class);
        LOG.info("Incoming author registration request "+request.toString());
        this.initDynamoDbClient();
        persistData(request);
        return ApiGatewayResponse.builder()
            .setStatusCode(201)
            .setHeaders(Collections.singletonMap("Content-Type", "application/json"))
            .setObjectBody(new CommonAPIResponse("Author registration successfully completed."))
            .build();
    }

    private String persistData(AuthorDto request) throws ConditionalCheckFailedException {
        String user_id = UUID.randomUUID().toString();
        Map<String, AttributeValue> attributesMap = new HashMap<>();
        attributesMap.put("id", new AttributeValue(user_id));
        attributesMap.put("firstName", new AttributeValue(request.getFirstName()));
        attributesMap.put("lastName", new AttributeValue(request.getLastName()));
        attributesMap.put("email", new AttributeValue(request.getEmail()));
        attributesMap.put("identification_number", new AttributeValue(request.getIdentificationNumber()));
        amazonDynamoDB.putItem(AUTHOR_DB_TABLE, attributesMap);
        return user_id;
    }

    private void initDynamoDbClient() {
        this.amazonDynamoDB = AmazonDynamoDBClientBuilder.standard()
            .withRegion(REGION)
            .build();
    }

}

Reading Environment Variables In AWS Lambda Java

In this API we are going to read REGION and table name from environment variables. This will make the developer’s life easier if we had to do a table name change in the future.

private String AUTHOR_DB_TABLE = System.getenv("AUTHOR_TABLE");

Sending JSON Response From AWS Lambda Java

By default, all the responses go from AWS lambda APIs using plain/text content type. Since we need to send all the responses as JSON to the consumers we have to set Content-type: application/json headers on all the API responses.

.setHeaders(Collections.singletonMap("Content-Type", "application/json"))

Read Author API Endpoint with FindAll and FindById - GET

  author_reads:
    handler: com.serverless.author.ReadHandler
    events:
      - httpApi:
          path: /authors
          method: get
package com.serverless.author;

import com.amazonaws.regions.Regions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
import com.amazonaws.services.dynamodbv2.model.GetItemResult;
import com.amazonaws.services.dynamodbv2.model.ScanRequest;
import com.amazonaws.services.dynamodbv2.model.ScanResult;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.serverless.ApiGatewayResponse;
import com.serverless.model.AuthorDto;
import com.serverless.model.CommonAPIResponse;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ReadHandler implements RequestHandler<APIGatewayProxyRequestEvent, ApiGatewayResponse> {

    private AmazonDynamoDB amazonDynamoDB;
    private String AUTHOR_DB_TABLE = System.getenv("AUTHOR_TABLE");
    private Regions REGION = Regions.fromName(System.getenv("REGION"));


    @Override public ApiGatewayResponse handleRequest(APIGatewayProxyRequestEvent input, Context context) {
        this.initDynamoDbClient();
        Map<String, String> queryParams = input.getQueryStringParameters();

        if (queryParams != null && queryParams.containsKey("findAll") && Boolean.parseBoolean(queryParams.get("findAll"))) {

            //Find All
            Map<String, AttributeValue> lastKeyEvaluated = null;
            List<AuthorDto> authorDtos = new ArrayList<>();
            do {
                ScanRequest scanRequest = new ScanRequest()
                    .withTableName(AUTHOR_DB_TABLE)
                    .withLimit(10)
                    .withExclusiveStartKey(lastKeyEvaluated);
                ScanResult result = amazonDynamoDB.scan(scanRequest);
                for (Map<String, AttributeValue> item : result.getItems()) {
                    authorDtos.add(mapToDto(item));
                }
                lastKeyEvaluated = result.getLastEvaluatedKey();
            } while (lastKeyEvaluated != null);

            return ApiGatewayResponse.builder()
                .setHeaders(Collections.singletonMap("Content-Type", "application/json"))
                .setObjectBody(authorDtos).setStatusCode(200).build();

        } else if (queryParams!= null && queryParams.containsKey("id") && queryParams.get("id") != null) {

            //Find by id
            Map<String, AttributeValue> attributesMap = new HashMap<>();
            attributesMap.put("id", new AttributeValue(queryParams.get("id")));
            GetItemRequest getItemRequest = new GetItemRequest().withTableName(AUTHOR_DB_TABLE)
                .withKey(attributesMap);
            GetItemResult item = amazonDynamoDB.getItem(getItemRequest);

            return ApiGatewayResponse.builder()
                .setHeaders(Collections.singletonMap("Content-Type", "application/json"))
                .setObjectBody(mapToDto(item.getItem())).setStatusCode(200).build();

        }

        return ApiGatewayResponse.builder()
            .setHeaders(Collections.singletonMap("Content-Type", "application/json"))
            .setObjectBody(new CommonAPIResponse("No data found under given query"))
            .setStatusCode(200).build();

    }

    private AuthorDto mapToDto(Map<String, AttributeValue> item) {
        AuthorDto authorDto = new AuthorDto();
        authorDto.setId(item.get("id").getS());
        authorDto.setEmail(item.get("email").getS());
        authorDto.setFirstName(item.get("firstName").getS());
        authorDto.setLastName(item.get("lastName").getS());
        authorDto.setIdentificationNumber(item.get("identification_number").getS());
        return authorDto;
    }

    private void initDynamoDbClient() {
        this.amazonDynamoDB = AmazonDynamoDBClientBuilder.standard()
            .withRegion(REGION)
            .build();
    }

}

Using Query Parameters with AWS Lambda Java

Here we need to bring a query parameter to identify what users requesting to read from DB to switch between findAll and findById.

In this case, we can use APIGatewayProxyRequestEvent which bundles with the AWS core library, to capture these params easily from the incoming requests.

Map<String, String> queryParams = input.getQueryStringParameters();
queryParams.get("findAll");

Update Author Endpoint - PATCH

  author_update:
    handler: com.serverless.author.UpdateHandler
    events:
      - httpApi:
          path: /authors/{id}
          method: patch

Here we are bringing requestBody and author id as path parameters. We can capture both using APIGatewayProxyRequestEvent as we did in READ endpoint.

package com.serverless.author;

import com.amazonaws.regions.Regions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.model.AttributeAction;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate;
import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.serverless.ApiGatewayResponse;
import com.serverless.model.AuthorDto;
import com.serverless.model.CommonAPIResponse;
import com.serverless.util.RequestConversionUtil;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class UpdateHandler implements RequestHandler<APIGatewayProxyRequestEvent, ApiGatewayResponse> {
    private AmazonDynamoDB amazonDynamoDB;
    private String AUTHOR_DB_TABLE = System.getenv("AUTHOR_TABLE");
    private Regions REGION = Regions.fromName(System.getenv("REGION"));

    @Override public ApiGatewayResponse handleRequest(APIGatewayProxyRequestEvent input, Context context) {
        initDynamoDbClient();
        RequestConversionUtil requestConversionUtil = new RequestConversionUtil();
        AuthorDto request = requestConversionUtil.parseRequestBody(input.getBody(), AuthorDto.class);

        Map<String, AttributeValue> keyMap = new HashMap<>();
        keyMap.put("id", new AttributeValue(input.getPathParameters().get("id")));

        UpdateItemRequest updateItemRequest = new UpdateItemRequest()
            .withTableName(AUTHOR_DB_TABLE)
            .addKeyEntry("id", new AttributeValue(input.getPathParameters().get("id")))
            .addAttributeUpdatesEntry("firstName",
                new AttributeValueUpdate(
                    new AttributeValue(request.getFirstName()),
                    AttributeAction.PUT))
            .addAttributeUpdatesEntry("lastName",
                new AttributeValueUpdate(
                    new AttributeValue(request.getLastName()),
                    AttributeAction.PUT))
            .addAttributeUpdatesEntry("email",
                new AttributeValueUpdate(
                    new AttributeValue(request.getEmail()),
                    AttributeAction.PUT))
            .addAttributeUpdatesEntry("identification_number",
                new AttributeValueUpdate(
                    new AttributeValue(request.getIdentificationNumber()),
                    AttributeAction.PUT));

        amazonDynamoDB.updateItem(updateItemRequest);

        return ApiGatewayResponse.builder()
            .setHeaders(Collections.singletonMap("Content-Type", "application/json"))
            .setObjectBody(new CommonAPIResponse("Author update successfully completed")).build();
    }

    private void initDynamoDbClient() {
        this.amazonDynamoDB = AmazonDynamoDBClientBuilder.standard()
            .withRegion(REGION)
            .build();
    }
}

Delete Author API Endpoint - DELETE


  author_delete:
    handler: com.serverless.author.DeleteHandler
    events:
      - httpApi:
          path: /authors/{id}
          method: delete
package com.serverless.author;

import com.amazonaws.regions.Regions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.serverless.ApiGatewayResponse;
import com.serverless.model.CommonAPIResponse;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class DeleteHandler implements RequestHandler<APIGatewayProxyRequestEvent, ApiGatewayResponse> {
    private AmazonDynamoDB amazonDynamoDB;
    private String AUTHOR_DB_TABLE = System.getenv("AUTHOR_TABLE");
    private Regions REGION = Regions.fromName(System.getenv("REGION"));

    @Override public ApiGatewayResponse handleRequest(APIGatewayProxyRequestEvent input, Context context) {
        initDynamoDbClient();

        Map<String, AttributeValue> keyMap = new HashMap<>();
        keyMap.put("id", new AttributeValue(input.getPathParameters().get("id")));

        DeleteItemRequest request = new DeleteItemRequest()
            .withTableName(AUTHOR_DB_TABLE)
            .withKey(keyMap);
        amazonDynamoDB.deleteItem(request);

        return ApiGatewayResponse.builder()
            .setHeaders(Collections.singletonMap("Content-Type", "application/json"))
            .setObjectBody(new CommonAPIResponse("Author deletion successfully completed")).build();

    }

    private void initDynamoDbClient() {
        this.amazonDynamoDB = AmazonDynamoDBClientBuilder.standard()
            .withRegion(REGION)
            .build();
    }
}

Finally, the completed serverless.yml should look like below, ensure you have written the configurations yml in correct level.

service: aws-lambda-serverless-crud-java

frameworkVersion: '3'

provider:
  name: aws
  runtime: java8
  stage: development
  region: us-west-2

  # ENVIRONMENT VARIABLES
  environment:
    REGION: ${opt:region, self:provider.region}
    AUTHOR_TABLE: javatodev-author-${opt:stage, self:provider.stage}

  # IAM ROLES TO ACCESS DYNAMODB TABLES
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:BatchGetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - !GetAtt AuthorDynamoDBTable.Arn

resources:
  Resources:
    AuthorDynamoDBTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: "id"
            AttributeType: "S"
        KeySchema:
          - AttributeName: "id"
            KeyType: "HASH"
        StreamSpecification:
          StreamViewType: "NEW_AND_OLD_IMAGES"
        TableName: ${self:provider.environment.AUTHOR_TABLE}

package:
  artifact: build/distributions/aws-serverless-crud-java.zip

functions:
  base_api:
    handler: com.serverless.Handler
    events:
      - httpApi:
          path: /
          method: get
  author_registration:
    handler: com.serverless.author.RegistrationHandler
    events:
      - httpApi:
          path: /authors/registration
          method: post
  author_reads:
    handler: com.serverless.author.ReadHandler
    events:
      - httpApi:
          path: /authors
          method: get
  author_update:
    handler: com.serverless.author.UpdateHandler
    events:
      - httpApi:
          path: /authors/{id}
          method: patch
  author_delete:
    handler: com.serverless.author.DeleteHandler
    events:
      - httpApi:
          path: /authors/{id}
          method: delete

All done now we have written the whole API with the necessary API endpoint exposing. We can deploy the API to AWS lambda.

$ sls deploy

API Deployment Output from AWS Lambda Serverless

AWS Lambda Function on the AWS dashboard

DynamoDB database created on AWS

Permissions were added on AWS Lambda to access DyanmoDB tables. - AWS lambda Java CRUD

Permissions were added on AWS Lambda to access DyanmoDB tables.

API Testings

We are using a Postman collection to test this API setup. You can sync the Postman collection with this link.

Author registration API Endpoint testing with postman - AWS lambda Java CRUD

Author registration API Endpoint testing with postman

Read All Authors API Endpoint - AWS lambda Java CRUD

Read All Authors API Endpoint

Conclusions

In this article, we have discussed how we can build a serverless API development using Java and DynamoDB and deploy with AWS lambda.

Also, there is one more article on Deploy Spring Boot Application On AWS Elastic Beanstalk.

The implementation of all these examples and code snippets can be found in our GitHub repository.

Happy coding.