Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 10, 2023 07:48 am GMT

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,

  • Java 8
  • AWS Lambda
  • Serverless
  • DynamoDB

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.

Image description

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

Image description

  • 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

Lets 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 use a separate Util class that 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 developers 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-javaframeworkVersion: '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.Arnresources:  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.zipfunctions:  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.

$ serverless 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.

API Testings

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

Image description

Image description

Conclusions

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

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

Happy coding.


Original Link: https://dev.to/aws-builders/aws-lambda-serverless-crud-api-with-java-189n

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To