How to develop AWS Lambda Serverless CRUD API With Java
- Chinthaka Dinadasa
- 10 Apr, 2023
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,
- Java 8
- AWS Lambda
- Serverless
- DynamoDB
topics that we are going to cover,
- Requirement For REST API Development
- Building Base Serverless Application
- DynamoDB tables and permission to access from serverless
- API Development
- DTO / Utils and Other Classes
- Base API Endpoint - GET
- Author Creation API Endpoint - POST
- Reading Environment Variables In AWS Lambda Java
- Sending JSON Response From AWS Lambda Java
- Read Author API Endpoint with FindAll and FindById - GET
- Using Query Parameters with AWS Lambda Java
- Update Author Endpoint - PATCH
- Delete Author API Endpoint - DELETE
- API Testings
- Conclusions
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 PATH | HTTP METHOD | DESCRIPTION |
/authors | GET | Read authors registered in the DB, Here this API should support both findAll and findById functions in single API path. |
/authors | POST | Create author data on dynamoDB database. |
/authors/:id | PATCH | Delete author data under the given ID. |
/authors/:id | DELETE | Read 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
-
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 Testings
We are using a Postman collection to test this API setup. You can sync the Postman collection with this link.
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.