CORS Tutorial for API Gateway and Lambda with React

CORS errors with API Gateway can be tricky. I’ll walk through setting up an API Gateway + Lambda backend that we’ll hit from a simple React front end.

Setting up the error

We can’t fix an error that we don’t have. First, let’s use the AWS SAM CLI to setup a simple API Gateway event that triggers a Lambda function. If you’ve never done that before, here’s a great tutorial from Thundra. Once configured, hit your API endpoint to make sure it works. I just used the default ‘Hello World!’ template.

Now, for a front end.

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
import axios from 'axios';

class App extends React.Component {
state = { text: '' };

componentDidMount() {
axios.get(`api gateway endpoint`)
.then(res => {
const text = res.data.message;
this.setState({ text });
})
}
render() {
return (
<div>
{ this.state.text }
</div>
)
}
}

export default App;
index.js
1
2
3
4
5
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';

ReactDOM.render(<App />, document.querySelector("#root"));

I use the create-react-app and have reduced the code down to bare bones.

Here’s the error we’ll fix.
cors error

What is CORS?

If you’re curious, here’s an in-depth description of what CORS is. In short, I’m at one domain and I want to request resources from another. In this case, I’m requesting from my local domain (localhost) to an API Gateway endpoint.

Enabling CORS

The AWS SAM CLI tool generates a default template for an API Gateway triggered Lambda function. We’ll need to edit this template and rebuild and redeploy to convert our API Gateway endpoint to allow for CORS. First, let’s start with the default SAM template.

default SAM template.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
lambda-apigateway-cors-blog

Sample SAM Template for lambda-apigateway-cors-blog

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3

Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.7
Events:
HelloWorld:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /hello
Method: get

Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
HelloWorldApi:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt HelloWorldFunctionRole.Arn

This template defines the HelloWorldFunction in the Resources section along with basic properties for that function. In that section, this template also defines an Event, which is the triggering event for the Lambda function.

We will flesh out the definition for that API Gateway resource and add an AWS::Include transform which will reference a swagger.yaml file that we’ve uploaded to an S3 bucket. A key part of this definition will be Cors: “‘*’” for the BasicAWSApiGateway resource.

We’ll also add RestApiId: !Ref BasicAWSApiGateway underneath the Properties attribute for the HelloWorld event.

Here’s the complete updated template.yaml file.

updated for CORS SAM template.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
lambda-apigateway-cors-blog

Sample SAM Template for lambda-apigateway-cors-blog

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3

Resources:
HelloWorldApiGateway:
Type: AWS::Serverless::Api
Properties:
Name: Gateway Endpoint HelloWorld
StageName: Prod
Cors: "'*'"
DefinitionBody:
'Fn::Transform':
Name: 'AWS::Include'
Parameters:
Location: s3://aws-sam-cli-managed-default-samclisourcebucket-kzrkk1luntm4/lambda-apigateway-cors-blog/swagger.yaml
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Runtime: python3.7
Events:
HelloWorld:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
RestApiId: !Ref HelloWorldApiGateway
Path: /hello
Method: get

Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
HelloWorldApiGateway:
Description: "API Gateway endpoint URL for Prod stage for Hello World function"
Value: !Sub "https://${HelloWorldApiGateway}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
HelloWorldFunction:
Description: "Hello World Lambda Function ARN"
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: "Implicit IAM Role created for Hello World function"
Value: !GetAtt HelloWorldFunctionRole.Arn

If we run sam build and sam deploy from the command line our serverless application won’t yet deploy because our template.yaml references a swagger.yaml file that doesn’t exist. Let’s build that now.

swagger.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
swagger: '2.0'
info:
description: 'This is a test setting up CORS for APIGatway and a Lambda event'
version: '1.0.0'
title: AutoGate Admin Service Management Gateway

paths:
/hello:
get:
responses:
200:
description: 200 response
headers:
Access-Control-Allow-Origin:
type: string
x-amazon-apigateway-integration:
uri:
Fn::Sub: 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations'
responses:
default:
statusCode: 200
responseParameters:
method.response.header.Access-Control-Allow-Origin: "'*'"
passthroughBehavior: when_no_match
httpMethod: POST
type: aws_proxy
options:
consumes:
- application/json
produces:
- application/json
responses:
200:
description: '200 response'
schema:
$ref: '#/definitions/Empty'
headers:
Access-Control-Allow-Origin:
type: string
Access-Control-Allow-Methods:
type: string
Access-Control-Allow-Headers:
type: string
security:
- None: []
x-amazon-apigateway-integration:
responses:
default:
statusCode: 200
responseParameters:
method.response.header.Access-Control-Allow-Methods: "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'"
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Origin: "'*'"
requestTemplates:
application/json: '{"statusCode": 200}'
passthroughBehavior: when_no_match
type: mock
definitions:
Empty:
type: object
title: Empty Schema

We’ll want to take note of this line Fn::Sub: ‘arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations’. Here, you’ll want to make sure you swap out your own function ARN value. That’s found under Resources in your template.yaml; it’s whatever you initially defined your function name as.

The namespace, which matches your app name, where we’ll upload this swagger file exists already in the bucket the SAM tool automatically generated when you first deployed the application. The name should begin with aws-sam-cli-managed-default-samclisourcebucket or something similar.

For our CORS activation needs, this swagger.yaml file defines the headers and values necessary for CORS to function.

Finally, we’ll want to add in a ‘headers’ section to the return value of our Lambda function.

1
2
3
4
5
6
7
8
9
10
11
{
"statusCode": 200,
'headers': {
"Access-Control-Allow-Origin": "*",
'Content-Type': 'application/json'
},
"body": json.dumps({
"message": "hello world",
# "location": ip.text.replace("\n", "")
}),
}

If all goes well, when you fire up your React app, you’ll see the much beloved hello world in the upper left hand corner of your browser.

NB: There are options to enable CORS via the AWS Console in API Gateway. I have never successfully enabled CORS via that route. This implementation is more straightforward: requiring only a few changes to the default SAM template, a swagger.yaml file and a small change to the Lambda function response.