1.2 Make our Lambda accessible from the internet with API Gateway
AWS Lambda

1.2 Make our Lambda accessible from the internet with API Gateway

Marcell Simon
Marcell Simon

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.

Function overview of the project

Configure API Gateway as a REST API and right now without any Security.

API Gateway configuration
"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
API Gateway trigger with the URL

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.

queryStringParameters: { symbol: 'BTCUSDT' }
multiValueQueryStringParameters: { symbol: [ 'BTCUSDT' ] }
Event query parameters with single params

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.

Test event configuration

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:

Succesful API response

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.

How to enable CORS

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.

CORS settings for the API Gateway

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.