1.2 Make our Lambda accessible from the internet with API Gateway
We created a Lambda in the previous post that downloads the price of Bitcoin. Now let's configure an API Gateway so we can call our Lambda from the internet.
What do we want to achieve?
When you are creating an online service you want to make it available to the world. You have to handle the traffic from the users to your backend application one way or another. The easiest way to make your Lambda available to anyone is to use API Gateway.
The two choices you have is HTTP API and REST API. They are very similar, but some features are only supported in one or the other. REST is older, but it still has some features that are not supported yet in HTTP API like TLS 1.0, API caching or WAF and many more. HTTP API on the other hand is faster and supports more recent technologies.
Add the API gateway
Just like we added the EventBridge trigger to call our code every 5 minutes, we can add a new Trigger.
Configure API Gateway as a REST API and right now without any Security.
"Deployment stage: A stage is a logical reference to a lifecycle state of your API (for example,dev
,prod
,beta
,v2
). API stages are identified by the API ID and stage name. They're included in the URL that you use to invoke the API. Each stage is a named reference to a deployment of the API and is made available for client applications to call." - AWS docs
After adding the API Gateway we can see the API endpoint in the Configuration. Opening that gives us the following error:
{"message": "Internal server error"}
Did we miss something? Actually no. If you look at the default example that Lambda gives us, you can see, that the return type handles the API response.
exports.handler = async (event) => {
// TODO implement
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
Between Lambdas you don't need to use this format, you can do it as you wish. But API Gateway forces you to specify the above format. So from the previous code:
exports.handler = async (event) => {
// get data
const result = await get(BINANCE_URL, { symbol: 'BTCUSDT' })
return result.price
}
Let's move to the API response format:
exports.handler = async (event) => {
// get data
const result = await get(BINANCE_URL, { symbol: 'BTCUSDT' })
return {
statusCode: 200,
body: JSON.stringify({price: result.price})
}
}
Now after deploying we can see our price in the browser.
{"price":"64424.56000000"}
Add the symbol to the request
First of all, let's see what our Lambda receives when we call the URL from the browser. Just add a console.log(event) to the beginning of the Lambda code and call the API Gateway.
https://crt04j31a1.execute-api.eu-west-1.amazonaws.com/test/bitcoin-price-downloader?symbol=BTCUSDT
The event will be the following:
{
resource: '/bitcoin-price-downloader',
path: '/bitcoin-price-downloader',
httpMethod: 'GET',
headers: {
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/
webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9,hu-HU;q=0.8,hu;q=0.7',
Host: 'crt04j31a1.execute-api.eu-west-1.amazonaws.com',
'sec-ch-ua': '"Google Chrome";v="89", "Chromium";v="89", ";Not A Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114
Safari/537.36',
'X-Amzn-Trace-Id': 'Root=1-6076a87f-46f0556250dab7aa22484ec1',
'X-Forwarded-For': '3.121.81.148',
'X-Forwarded-Port': '443',
'X-Forwarded-Proto': 'https'
},
multiValueHeaders: {
accept: [
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/
webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9'
],
'accept-encoding': [ 'gzip, deflate, br' ],
'accept-language': [ 'en-US,en;q=0.9,hu-HU;q=0.8,hu;q=0.7' ],
Host: [ 'crt04j31a1.execute-api.eu-west-1.amazonaws.com' ],
'sec-ch-ua': [
'"Google Chrome";v="89", "Chromium";v="89", ";Not A Brand";v="99"'
],
'sec-ch-ua-mobile': [ '?0' ],
'sec-fetch-dest': [ 'document' ],
'sec-fetch-mode': [ 'navigate' ],
'sec-fetch-site': [ 'none' ],
'sec-fetch-user': [ '?1' ],
'upgrade-insecure-requests': [ '1' ],
'User-Agent': [
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'
],
'X-Amzn-Trace-Id': [ 'Root=1-6076a87f-46f0556250dab7aa22484ec1' ],
'X-Forwarded-For': [ '3.121.81.148' ],
'X-Forwarded-Port': [ '443' ],
'X-Forwarded-Proto': [ 'https' ]
},
queryStringParameters: { symbol: 'BTCUSDT' },
multiValueQueryStringParameters: { symbol: [ 'BTCUSDT' ] },
pathParameters: null,
stageVariables: null,
requestContext: {
resourceId: 'e5eki6',
resourcePath: '/bitcoin-price-downloader',
httpMethod: 'GET',
extendedRequestId: 'dw9D3FKkjoEFwUQ=',
requestTime: '14/Apr/2021:08:31:59 +0000',
path: '/test/bitcoin-price-downloader',
accountId: '624981414257',
protocol: 'HTTP/1.1',
stage: 'test',
domainPrefix: 'crt04j31a1',
requestTimeEpoch: 1618389119040,
requestId: 'f4793d38-59ad-419c-b3b7-098fd2882286',
identity: {
cognitoIdentityPoolId: null,
accountId: null,
cognitoIdentityId: null,
caller: null,
sourceIp: '3.121.81.148',
principalOrgId: null,
accessKey: null,
cognitoAuthenticationType: null,
cognitoAuthenticationProvider: null,
userArn: null,
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114
Safari/537.36',
user: null
},
domainName: 'crt04j31a1.execute-api.eu-west-1.amazonaws.com',
apiId: 'crt04j31a1'
},
body: null,
isBase64Encoded: false
}
That's a lot of information. But for now, the most important part is the query string.
Multivalue means that the query parameter can be an array. So if we want to send multiple symbols to our Lambda, we can do it like:
https://crt04j31a1.execute-api.eu-west-1.amazonaws.com/test/bitcoin-price-downloader?symbols=BTCUSDT&symbols=ETHUSDT
and in the `multiValueQueryStringParameters` the symbols array will contain both, while `queryStringParameters` will only contain the last one.
queryStringParameters: { symbols: 'ETHUSDT' }
multiValueQueryStringParameters: { symbols: [ 'BTCUSDT', 'ETHUSDT' ] }
But let's use only one symbol for now.
Let's modify the code. I added the query parameter handling and also some error handling for Binance as we can input any random data.
If there's an error in the Binance request it'll return a 200 OK response with a JSON containing a code and a message (msg). For now we can output this.
const BINANCE_URL = `https://api.binance.com/api/v3/ticker/price`
exports.handler = async (event) => {
const symbol = event.queryStringParameters && event.queryStringParameters.symbol
// handle missing parameter
if (!symbol) {
return {
statusCode: 404,
body: JSON.stringify({error: 'Symbol query parameter not defined'})
}
}
// get data
const result = await get(BINANCE_URL, { symbol })
if (result.code) {
return {
statusCode: 400,
body: JSON.stringify({error: result.msg})
}
}
return {
statusCode: 200,
body: JSON.stringify({price: result.price})
}
}
To test the Lambda itself now without using the API Gateway, we can edit the test configuration. Next to the Test button open the dropdown and press Configure Test Event.
We can add the minimal configuration for this test to run which is just the queryStringParameters with the one symbol. By saving and then initiation a Test, we can see the results:
And calling it with an invalid symbol like EURUSD (which is not available on Binance) we get an error:
{"error":"Invalid symbol."}
Enabling CORS
If you want to call your Gateway from a website the first problem you will face is CORS.
CORS: "Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any other origins (domain, scheme, or port) than its own from which a browser should permit loading of resources." - MDN Web docs
On the API Gateway's config, you can find the Enable CORS option for your Gateway.
The best is if you specify your site's URL, but you can also use the provided '*' wildcard so it will allow all websites to access your site.
You can also set up domains and all kinds of cool stuff, but we'll get back to that later.
Next time we'll connect to a Database to experience the beauty and the pain of the VPCs.