When creating a new WebSocket API in AWS Api Gateway, AWS will automatically generate a URL for you to connect to. The format is not very user-friendly though, it’s not something you can easily remember and will change if you need to re-create the Api Gateway for some reason. In this article, we’ll see how to use a custom domain name with a WebSocket API in AWS Api Gateway.
TOC
Prerequisites
I’m using Pulumi for writing the infrastructure code. The concepts in this article are the same if you’re using CloudFormation, Terraform or clicking through the AWS Console.
Assuming you have a WebSocket API already created in AWS Api Gateway. If you don’t have one, you can follow the steps in the previous article or use the command below to clone the repository . It goes without saying that you also need to have a domain name that you own.
git clone git@github.com:exanubes/api-gateway-websocket-api.git
Custom domain
By default, the WebSocket Api is available at a generated url that looks something like https://<api-id>.execute-api.<region>.amazonaws.com/<stage>
.
If you want to use a custom domain you can do so by creating a new domain name inside API Gateway – this has to be a domain that you already own either through
Route53 or another domain registrar.
const domainName = new aws.apigatewayv2.DomainName('domain-name', {
domainName: 'exanub.es',
domainNameConfiguration: {
certificateArn: CERTIFICATE_ARN,
endpointType: 'REGIONAL',
securityPolicy: 'TLS_1_2'
}
});
const domainName = new aws_native.apigatewayv2.DomainName('domain-name', {
domainName: 'exanub.es',
domainNameConfigurations: [
{
certificateArn: CERTIFICATE_ARN,
endpointType: 'REGIONAL'
}
]
});
Then, in the hosted zone of your domain, you need to create an ALIAS record to that api gateway domain name.
new aws.route53.Record('alias', {
name: 'exanub.es',
type: aws.route53.RecordType.A,
zoneId: HOSTED_ZONE_ID,
aliases: [
{
name: domainName.domainNameConfiguration.targetDomainName,
zoneId: domainName.domainNameConfiguration.hostedZoneId,
evaluateTargetHealth: true
}
]
});
new aws.route53.Record('alias', {
name: 'exanub.es',
type: aws.route53.RecordType.A,
zoneId: HOSTED_ZONE_ID,
aliases: [
{
name: domainName.regionalDomainName,
zoneId: domainName.regionalHostedZoneId,
evaluateTargetHealth: true
}
]
});
Here, I’m directing traffic to the domain name that I’ve just created within API Gateway using its regional domain name and hosted zone. The regional domain name is the endpoint that AWS API Gateway provides for a custom domain name in a specific AWS region. This domain name allows clients to directly access the API hosted in the specified AWS region. You might have to wait a few minutes for the ALIAS record to work after initial deployment as changes to DNS settings are not usually immediate.
API Mapping Key
I don’t want the domain apex to be used for establishing the WebSocket connection, so I will create an API Mapping and make the
websocket endpoint to be exanub.es/chat
.
const apiMapping = new aws.apigatewayv2.ApiMapping('api-mapping', {
apiId: api.id,
domainName: domainName.domainName,
stage: stage.name,
apiMappingKey: 'chat'
});
const apiMapping = new aws_native.apigatewayv2.ApiMapping('api-mapping', {
apiId: api.id,
domainName: domainName.domainName,
stage: stage.name,
apiMappingKey: 'chat'
});
Worth noting that even though the mapping key is set to chat
for the custom domain. It’s still bound to a stage, so when connecting
to example.com/chat
it will use the dev
stage. You will need a different mapping for other stages.
Stage Variables
Now, instead of hardcoding the domain name and api mapping key, we can use stage variables which are actually more convenient than environment variables in this case considering we only have to assign the variables once per stage rather than once per function and it’s also easier to reuse the functions across different stages without risking that test data is saved in a production table for example.
const stage = api.addStage("dev", {
stageVariables: {
CONNECTIONS_TABLE: table.arn,
API_KEY_MAPPING: 'chat'
}
});
Going back to when we created a stage, let’s add stageVariables
prop and pass both the CONNECTIONS_TABLE
and API_KEY_MAPPING
.
With this out of the way, let’s go to the message
lambda and replace the invoke api url and table arn with the stage variables.
const handler = async function handler(event) {
const table = event.stageVariables?.CONNECTIONS_TABLE;
const apiKeyMapping = event.stageVariables?.API_KEY_MAPPING;
const body = event.body ? JSON.parse(event.body) : null;
if (!body) throw new Error("No body");
if (!table) throw new Error("table cannot be undefined");
if (!apiKeyMapping) throw new Error("apiMappingKey cannot be undefined");
const endpoint = `https://${event.requestContext.domainName}/${apiKeyMapping}`;
//....
} satisfies APIGatewayProxyWebsocketHandlerV2;
The base url in event.requestContext.domainName
is now set to the domain you assigned to your API, and instead of using the API stage name we’re using the
API Mapping Key that we’ve created earlier. There’s one problem though, the auto-generated invoke url is still active, anyone can use that instead of our own
domain name. This means event.requestContext.domainName
can be the auto-generated url instead of the custom domain leading to errors.
Disable generated invoke url
To prevent that, we can add disableExecuteApiEndpoint
to the Api Gateway resource and set it to true
. This will disable the auto-generated invoke url.
const api = new WebsocketApi("websocket-api", {
name: "exanubes-websocket-api",
routeSelectionExpression: "$request.body.type",
disableExecuteApiEndpoint: true,
});
Conclusion
Integrating a custom domain name with a WebSocket API in AWS API Gateway significantly enhances the user experience by providing a more memorable and stable URL. By using infrastructure as code tools like Pulumi, you can automate this process, ensuring a seamless setup and management of custom domains. Key steps include creating a custom domain, setting up an ALIAS record in Route53, and using API mappings and stage variables for flexible endpoint management. Disabling the default execute API endpoint further secures your API by enforcing the use of your custom domain. This approach not only improves usability but also aligns with best practices for managing API endpoints in a production environment.