AWS SAM stands for AWS Serverless Application Model. It is created by AWS to make deployment of AWS Lambda functions easier. With AWS SAM you can describe your serverless application as a YAML file. Then using AWS SAM CLI, you can use this YAML file to deploy all the necessary AWS resources required by your application. Under the hood, AWS SAM CLI generates a Cloud Formation template, which is then used to create and modify the resources required for the application.

In this article we will see how to create a REST API using AWS SAM CLI using the Java programming language. There are some tutorials on the web which show this, however these tutorials don’t show how to handle some common aspects such as CORS implementation. Recently I had to deploy a quick API using AWS SAM and Java, however, I couldn’t find an easy to follow tutorial on how to handle CORS implementation with this. After doing some trial and error with some Stack Overflow answers, I managed to get it to work properly. Hence I thought it’s better to write a tutorial to cover CORS implementation also along with a basic REST API. So let’s get started.

Before starting you will need to ensure that you have AWS CLI, AWS SAM CLI and JDK installed and configured on your system. For this tutorial I am using JDK 11. As of September 2022, it seems that AWS Lambda doesn’t support JDK 17 runtime. This may change in future. If you want to test your API locally, you will also need Docker Desktop installed. Also ensure that you have an AWS account configured to use with your AWS CLI using the aws configure command.

Source Code

You can find the complete source code for this tutorial here: https://github.com/MobisoftInfotech/java-sam-api-example

Generate Application Skeleton using AWS SAM CLI

Once all the required software is installed, you can generate a java based AWS SAM application using the following command:

sam init -r java11 -d maven --app-template hello-world -n java-sam-api-example

This will create a directory named java-sam-api-example and will generate the initial sources. It generates following files:

java-sam-api-example
├── HelloWorldFunction
│   ├── pom.xml
│   └── src
│       ├── main
│       │   └── java
│       │       └── helloworld
│       │           └── App.java
│       └── test
│           └── java
│               └── helloworld
│                   └── AppTest.java
├── README.md
├── events
│   └── event.json
└── template.yaml

Unfortunately SAM CLI hardcodes the word “hello world” all over your code. So you need to manually change it everywhere. So I did that as per our application’s needs and this is how it looks after the changes:

java-sam-api-example
├── README.md
├── SAMExampleFunction
│   ├── pom.xml
│   └── src
│       ├── main
│       │   └── java
│       │       └── samexample
│       │           └── App.java
│       └── test
│           └── java
│               └── samexample
│                   └── AppTest.java
├── events
│   └── event.json
└── template.yaml

I have renamed the “HelloWorldFunction” directory to “SAMExampleFunction” and also changed package name from helloworld to “samexample”. Of course you should use a name relevant to your project here.

Here since we have changed the package name to “samexample”, we will need to change the “App.java” and “AppTest.java” files also to change the package line from:

package helloworld;

to:

package samexample;

One of the most important files here is the template.yaml. It describes the serverless application. Again unfortunately SAM CLI hard codes the word “hello world” everywhere in this template. So let’s first modify this as per our needs so that it looks like the following:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  java-sam-api-example

  Sample SAM Template for java-sam-api-example

Globals:
  Function:
    Timeout: 20

Resources:
  SAMExampleFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: SAMExampleFunction
      Handler: samexample.App::handleRequest
      Runtime: java11
      Architectures:
        - x86_64
      MemorySize: 512
      Environment: 
        Variables:
          PARAM1: VALUE
          JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
      Events:
        SAMExample:
          Type: Api 
          Properties:
            Path: /hello
            Method: get

Outputs:
  SAMExampleApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  SAMExampleFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt SAMExampleFunction.Arn
  SAMExampleFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt SAMExampleFunctionRole.Arn

At this point you can build and run the application locally using the commands:

sam build
sam local start-api

Now of you test the API using following command:

curl --location --request GET 'http://127.0.0.1:3000/hello'

You should see following output:

{ "message": "hello world", "location": "103.254.55.17" }

As you may already know, to expose a lambda function as HTTP endpoint, you need AWS API Gateway to be configured. In this “hello world” template, it uses an implicit gateway resource named: ServerlessRestApi. As it turns out to support CORS you need to modify the API gateway resource. So we can’t use this implicit API gateway resource. So we will create a API resource in our template.yaml file:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
 java-sam-api-example

 Sample SAM Template for java-sam-api-example

Globals:
 Function:
   Timeout: 20

Resources:
 SAMExampleApi:
   Type: AWS::Serverless::Api
   Properties:
     StageName: Prod
     Cors:
       AllowMethods: "'GET,POST,OPTIONS,PUT,PATCH,DELETE'"
       AllowHeaders: "'content-type'"
       AllowOrigin: "'*'"
       AllowCredentials: "'*'"
 SAMExampleFunction:
   Type: AWS::Serverless::Function
   Properties:
     CodeUri: SAMExampleFunction
     Handler: samexample.App::handleRequest
     Runtime: java11
     Architectures:
       - x86_64
     MemorySize: 512
     Environment:
       Variables:
         PARAM1: VALUE
         JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
     Events:
       SAMExample:
         Type: Api
         Properties:
           Path: /hello
           Method: get
           RestApiId: !Ref SAMExampleApi

Outputs:
 SAMExampleApi:
   Description: "API Gateway endpoint URL for Prod stage for Hello World function"
   Value: !Sub "https://${SAMExampleApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
 SAMExampleFunction:
   Description: "Hello World Lambda Function ARN"
   Value: !GetAtt SAMExampleFunction.Arn
 SAMExampleFunctionIamRole:
   Description: "Implicit IAM Role created for Hello World function"
   Value: !GetAtt SAMExampleFunctionRole.Arn

Here as you can notice we have added a new resource:

 SAMExampleApi:
   Type: AWS::Serverless::Api
   Properties:
     StageName: Prod
     Cors:
       AllowMethods: "'GET,POST,OPTIONS,PUT,PATCH,DELETE'"
       AllowHeaders: "'content-type,X-Custom-Header'"
       AllowOrigin: "'*'"
       AllowCredentials: "'*'"

Here I am using all HTTP methods in the “AllowMethods” and allowing “*” (any) origin in the “AllowOrigin” header. However for production deployments you should restrict the HTTP methods to the methods your API supports and the origin to your “production” domain only. In the AllowHeaders, you need to mention all the custom headers your API supports.

Then in the Events under “SAMExampleFunction” we have added new line:

RestApiId: !Ref SAMExampleApi

Also in the “Outputs” section we have updated the “Value” for the “SAMExampleApi” like follows:

  Value: !Sub "https://${SAMExampleApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

To support CORS we will need to make some changes to our Java code also. So we need to add the following lines in the “handleReuest” method of the App.java file:

headers.put("Access-Control-Allow-Origin", "*");
headers.put("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,PATCH,DELETE");
headers.put("Access-Control-Allow-Headers", "content-type,X-Custom-Header");

just below the line:

headers.put("X-Custom-Header", "application/json");

At this point we are ready to deploy our application so let’s run the following commands:

sam build
sam deploy --guided

At this point SAM CLI will ask you a bunch of questions about how to deploy your serverless application. Here are the answers I provided:

Setting default arguments for 'sam deploy'
=========================================
Stack Name [sam-app]: java-sam-api-example
AWS Region [us-west-2]:
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: Y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: N
SAMExampleFunction may not have authorization defined, Is this okay? [y/N]: y
Save arguments to configuration file [Y/n]: Y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:

…
…

Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: Y

Now SAM CLI will generate the CloudFormation template and will deploy a CloudFormation stack for the same. Once the deployment is complete, you should see the output similar to the following:

Outputs
------------------------------------------------------------------------------------------------------
Key                 SAMExampleFunctionIamRole
Description         Implicit IAM Role created for Hello World function
Value               arn:aws:iam::615447684439:role/java-sam-api-example-SAMExampleFunctionRole-
WKAFDXI4UXXG

Key                 SAMExampleApi
Description         API Gateway endpoint URL for Prod stage for Hello World function
Value               https://572qk46wc9.execute-api.us-west-2.amazonaws.com/Prod/hello/

Key                 SAMExampleFunction
Description         Hello World Lambda Function ARN
Value               arn:aws:lambda:us-west-2:615447684439:function:java-sam-api-example-
SAMExampleFunction-T19RPWh6SzlB
------------------------------------------------------------------------------------------------------

From the output you can get the URL for your API.

You can create a quick local file ‘index.html’ with the following content to ensure that your API indeed supports CORS properly:

<script>
const requestOptions = {
   method: 'GET',
   headers: { 'Content-Type': 'application/json' }
};
fetch('https://572qk46wc9.execute-api.us-west-2.amazonaws.com/Prod/hello/', requestOptions)
   .then(response => console.log(response))
   .catch(err => console.log(err));
</script>

You can use any local static file server like Apache, Nginx to access this index.html file locally. I use a NPM package called “serve” for this purpose. You can install it with the command

npm install -g serve

Then in any directory you can run the command “serve” to serve that directory on localhost:3000 by default. When I do this and open the URL http://localhost:3000/ in the browser, I see in the browser console that the API call succeeds and the CORS is working properly.

So that’s how we can create a Serverless Java API with AWS SAM CLI with CORS support. Now to destroy all the resources we created we can run the command:

sam delete

It will ask you to confirm the deletion and once confirmed will destroy all the resources created on AWS.

Author's Bio:

mobisoft-pritam
Pritam Barhate

Pritam Barhate, with an experience of 14+ years in technology, heads Technology Innovation at Mobisoft Infotech. He has a rich experience in design and has been a consultant for a variety of industries and startups. At Mobisoft Infotech, he primarily focuses on technology resources and develops the most advanced solutions.