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:
Pritam Barhate, with an experience of 14+ years in technology, heads Technology Innovation at Mobisoft Infotech. He has a rich experience in design and development. He 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.