Amazon API Gateway provides robust CORS support with significant differences between its two API types. This guide covers modern, secure CORS implementations using Infrastructure as Code and best practices.
AWS API Gateway has two types:
Choose HTTP API unless you need REST API-specific features.
Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.
HTTP API provides simpler CORS configuration with automatic OPTIONS handling.
https://example.comGET,POST,PUT,DELETEContent-Type,Authorization86400That's it! HTTP API handles OPTIONS preflight requests automatically.
Infrastructure as Code is the recommended approach for production deployments:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
# HTTP API (Simple, Recommended)
MyHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowOrigins:
- https://example.com
- https://app.example.com
AllowMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
AllowHeaders:
- Content-Type
- Authorization
MaxAge: 86400
AllowCredentials: true
# Lambda Function
MyFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.11
Handler: index.handler
InlineCode: |
import json
def handler(event, context):
return {
'statusCode': 200,
'body': json.dumps({'message': 'Success'})
}
Events:
ApiEvent:
Type: HttpApi
Properties:
ApiId: !Ref MyHttpApi
Path: /data
Method: get
import * as cdk from 'aws-cdk-lib';
import * as apigatewayv2 from '@aws-cdk/aws-apigatewayv2-alpha';
import * as lambda from 'aws-cdk-lib/aws-lambda';
export class CorsStack extends cdk.Stack {
constructor(scope: cdk.App, id: string) {
super(scope, id);
// HTTP API (Recommended - Simpler)
const httpApi = new apigatewayv2.HttpApi(this, 'HttpApi', {
apiName: 'my-http-api',
corsPreflight: {
allowOrigins: [
'https://example.com',
'https://app.example.com'
],
allowMethods: [
apigatewayv2.CorsHttpMethod.GET,
apigatewayv2.CorsHttpMethod.POST,
apigatewayv2.CorsHttpMethod.PUT,
apigatewayv2.CorsHttpMethod.DELETE
],
allowHeaders: ['Content-Type', 'Authorization'],
maxAge: cdk.Duration.days(1),
allowCredentials: true
}
});
}
}
resource "aws_apigatewayv2_api" "http_api" {
name = "my-http-api"
protocol_type = "HTTP"
cors_configuration {
allow_origins = [
"https://example.com",
"https://app.example.com"
]
allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allow_headers = ["Content-Type", "Authorization"]
max_age = 86400
allow_credentials = true
}
}
resource "aws_apigatewayv2_integration" "lambda" {
api_id = aws_apigatewayv2_api.http_api.id
integration_type = "AWS_PROXY"
integration_uri = aws_lambda_function.my_function.invoke_arn
}
resource "aws_apigatewayv2_route" "get_data" {
api_id = aws_apigatewayv2_api.http_api.id
route_key = "GET /data"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
REST API requires more complex CORS configuration with manual OPTIONS method setup.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
MyRestApi:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Cors:
AllowOrigin: "'https://example.com'"
AllowHeaders: "'Content-Type,Authorization'"
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
MaxAge: "'86400'"
AllowCredentials: true
# Add CORS to error responses
GatewayResponses:
DEFAULT_4XX:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'https://example.com'"
gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type,Authorization'"
DEFAULT_5XX:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'https://example.com'"
gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type,Authorization'"
MyFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.11
Handler: index.handler
InlineCode: |
import json
def handler(event, context):
return {
'statusCode': 200,
'body': json.dumps({'message': 'Success'})
}
Events:
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref MyRestApi
Path: /data
Method: get
import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
export class CorsStack extends cdk.Stack {
constructor(scope: cdk.App, id: string) {
super(scope, id);
// REST API (More Features)
const restApi = new apigateway.RestApi(this, 'RestApi', {
restApiName: 'my-rest-api',
defaultCorsPreflightOptions: {
allowOrigins: [
'https://example.com',
'https://app.example.com'
],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
maxAge: cdk.Duration.days(1),
allowCredentials: true
}
});
// Lambda integration
const handler = new lambda.Function(this, 'Handler', {
runtime: lambda.Runtime.PYTHON_3_11,
code: lambda.Code.fromInline(`
import json
def handler(event, context):
return {
'statusCode': 200,
'body': json.dumps({'message': 'Success'})
}
`),
handler: 'index.handler'
});
const resource = restApi.root.addResource('data');
resource.addMethod('GET', new apigateway.LambdaIntegration(handler));
}
}
resource "aws_api_gateway_rest_api" "rest_api" {
name = "my-rest-api"
}
resource "aws_api_gateway_resource" "resource" {
rest_api_id = aws_api_gateway_rest_api.rest_api.id
parent_id = aws_api_gateway_rest_api.rest_api.root_resource_id
path_part = "data"
}
# OPTIONS method for preflight
resource "aws_api_gateway_method" "options" {
rest_api_id = aws_api_gateway_rest_api.rest_api.id
resource_id = aws_api_gateway_resource.resource.id
http_method = "OPTIONS"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "options" {
rest_api_id = aws_api_gateway_rest_api.rest_api.id
resource_id = aws_api_gateway_resource.resource.id
http_method = aws_api_gateway_method.options.http_method
type = "MOCK"
request_templates = {
"application/json" = "{\"statusCode\": 200}"
}
}
resource "aws_api_gateway_method_response" "options_200" {
rest_api_id = aws_api_gateway_rest_api.rest_api.id
resource_id = aws_api_gateway_resource.resource.id
http_method = aws_api_gateway_method.options.http_method
status_code = "200"
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = true
"method.response.header.Access-Control-Allow-Methods" = true
"method.response.header.Access-Control-Allow-Origin" = true
}
}
resource "aws_api_gateway_integration_response" "options_200" {
rest_api_id = aws_api_gateway_rest_api.rest_api.id
resource_id = aws_api_gateway_resource.resource.id
http_method = aws_api_gateway_method.options.http_method
status_code = aws_api_gateway_method_response.options_200.status_code
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = "'Content-Type,Authorization'"
"method.response.header.Access-Control-Allow-Methods" = "'GET,POST,PUT,DELETE,OPTIONS'"
"method.response.header.Access-Control-Allow-Origin" = "'https://example.com'"
}
}
For advanced scenarios, handle CORS in your Lambda function for complete control:
import json
def lambda_handler(event, context):
# Get origin from request
origin = event.get('headers', {}).get('origin', '')
# Validate origin
allowed_origins = [
'https://example.com',
'https://app.example.com'
]
cors_headers = {}
if origin in allowed_origins:
cors_headers = {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
'Access-Control-Max-Age': '86400',
'Access-Control-Allow-Credentials': 'true',
'Vary': 'Origin'
}
# Handle preflight
request_context = event.get('requestContext', {})
http_method = request_context.get('http', {}).get('method') or request_context.get('httpMethod')
if http_method == 'OPTIONS':
return {
'statusCode': 204,
'headers': cors_headers,
'body': ''
}
# Handle actual request
return {
'statusCode': 200,
'headers': {
**cors_headers,
'Content-Type': 'application/json'
},
'body': json.dumps({'message': 'Success'})
}
exports.handler = async (event) => {
const origin = event.headers?.origin || '';
const allowedOrigins = [
'https://example.com',
'https://app.example.com'
];
const corsHeaders = {};
if (allowedOrigins.includes(origin)) {
corsHeaders['Access-Control-Allow-Origin'] = origin;
corsHeaders['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS';
corsHeaders['Access-Control-Allow-Headers'] = 'Content-Type,Authorization';
corsHeaders['Access-Control-Max-Age'] = '86400';
corsHeaders['Access-Control-Allow-Credentials'] = 'true';
corsHeaders['Vary'] = 'Origin';
}
// Handle preflight
const httpMethod = event.requestContext?.http?.method ||
event.requestContext?.httpMethod;
if (httpMethod === 'OPTIONS') {
return {
statusCode: 204,
headers: corsHeaders,
body: ''
};
}
// Handle actual request
return {
statusCode: 200,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: 'Success' })
};
};
After making CORS changes in the console for REST APIs, you MUST deploy:
aws apigateway create-deployment \ --rest-api-id YOUR_API_ID \ --stage-name prod
REST API only adds CORS headers to 200 responses by default. Configure gateway responses for error codes:
# SAM template
GatewayResponses:
DEFAULT_4XX:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'https://example.com'"
gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type'"
DEFAULT_5XX:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'https://example.com'"
gatewayresponse.header.Access-Control-Allow-Headers: "'Content-Type'"
This combination is INVALID per CORS specification:
# INVALID - Will fail in browsers AllowOrigin: "'*'" AllowCredentials: true
Must use specific origin:
# VALID AllowOrigin: "'https://example.com'" AllowCredentials: true
| Feature | HTTP API | REST API |
|---|---|---|
| CORS Configuration | Simple, built-in | Complex, manual OPTIONS |
| Price | Cheaper (~70% less) | More expensive |
| Latency | Lower | Higher |
| API Keys | No | Yes |
| Usage Plans | No | Yes |
| Request Validation | No | Yes |
# For HTTP API aws apigatewayv2 update-stage \ --api-id YOUR_API_ID \ --stage-name '$default' \ --access-log-settings DestinationArn=arn:aws:logs:region:account:log-group:name # View logs aws logs tail /aws/apigateway/YOUR_API_ID --follow
# Get HTTP API details aws apigatewayv2 get-api --api-id YOUR_API_ID # Get REST API details aws apigateway get-rest-api --rest-api-id YOUR_API_ID
For comprehensive testing instructions including curl commands, browser DevTools usage, and troubleshooting common CORS errors, see the CORS Testing Guide.
The content on this site stays fresh thanks to help from users like you! If you have suggestions or would like to contribute, fork us on GitHub.
Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain