Sunday, 21 March 2021

One Lambda to Serve Them All

 The really cheap way to host a dynamic web site on AWS is by using the combination of S3 and Lambda:

  • S3 to store the static contents such as HTML files, graphics, CSS, javascript files
  • Lambda and API Gateway to serve the dynamic contents by providing APIs to the javascripts of the web site.
I went one step further with my Numbers Game single page web application by serving everything from a Lambda function without having to use S3. This is because
  1. I want the web site to stay totally free. Lambda is perpetually free tier and I don't get charged as long as my volumes stay under the threshold. However, S3 is not free after the first 12 months.
  2. The static content for my web page is relatively simple - one HTML file actually. My own Javascripts are embedded in the page; 3rd-party contents (CSS, javascripts, icons) are pointed to their corresponding URLs so the browser will simply gets them from the internet.
  3. I don't want to enable CORS of my AWS API, so my web page (and its javascript) and the APIs must be served by the same domain. 
This is very easy to do with AWS Lambda: my Lambda function handles two types of requests - GET and POST (of course, you can refactor these to let the API layer do some mapping and use two Lambda functions... ). 
  • for GET, the Lambda function returns the HTML string. This string was prepared by feeding my static HTML file to some online converter to get the big string with all the double quotes (along with some other characters) escaped - the online tool's result was not perfect, I had to hand fix something that it missed. This HTML page basically allows the user to enter the input numbers and performs some validation according to the game's rules. Once submitted, it will invoke the API using POST
  • for POST, the Lambda function invokes the javascript functions to calculate the results and return them as a JSON string. The HTML simply displays the results on the same page.
The Lambda function:

exports.handler = async (event) => {
	console.log("event:"+JSON.stringify(event));
	var answer="no or unsupported event";
	var response;
	if(event.httpMethod == "GET") {
		response = {
	        statusCode: 200,
	        headers: {
	        	"content-type": "text/html"
	        },
	        body: myHtml,
	    };
	} else if (event.httpMethod == "POST" || event.routeKey.startsWith("POST")) { // supporting REST APIs and HTTP APIs
		if(event.body != null) {
			var e=JSON.parse(event.body);
			var numbers=[e.n1,e.n2,e.n3,e.n4,e.n5,e.n6], target=e.target;
			answer=solveAllGames(numbers, target);
			//answer=solveGame(numbers, target);
		}
	    response = {
	        statusCode: 200,
	        body: JSON.stringify(answer),
			headers: {
	        	"Access-Control-Allow-Origin": "*"
	        }
	    };
	}
    console.log(response);
    return response;
};
//... more code here ... where myHtml stores the big string converted from my static HTML file.

This approach works quite well. However, it is an abomination and wrong in so many places:
  • it's mixing network layer logic with business logic - in the code it's handling http methods which should be the responsibility of the API Gateway!
  • At the API, when only the ANY is mapped, the event passed into the Lambda is at a higher level - the Lambda function has to traverse into the 'body' to get the input parameters. 
  • The JSON.parse(event.body) is very cumbersome. I should be able to just get the input numbers from the event itself as a JSON object, not as a string. 
  • the lambda function cannot be used as-is for other triggering types (because it explicitly handles HTTP methods). e.g. cannot test in AWS console.
It's time to refactor... 💨

See also:

No comments: