loading...
Cover image for Serverless Tutorial: Build a serverless leaderboard
Lambda Store

Serverless Tutorial: Build a serverless leaderboard

svenanderson profile image Sven Anderson Updated on ・5 min read

Leaderboard is a popular term in gaming world but most of the modern web sites have a variations of a leaderboard such as 'most read news' in BBC news or 'most popular gift' in Amazon.com. The problem is the same: Each item has a score and the list needs to be sorted with the score. The list can be huge and you want to access to top n items with very low latency.

In this tutorial: We will build a simple leaderboard using AWS Lambda (using Node.js) and serverless Redis. You can find the source code here

Prerequisites:

  • An AWS account for AWS Lambda functions.

  • Install AWS SAM CLI tool as described here to create and deploy the project.

  • A Lambda Store account for serverless Redis.

Create the project

We will use AWS SAM to create and deploy the project. Here the commands

  • Create a new folder for your project and inside the folder run sam init
  • Select Custom Template Location
  • Enter https://github.com/Lambda-Store/serverless-leaderboard

SAM will download the project. Now let's take a look inside:

Functions

You will see 3 AWS Lambda functions under 'src>handlers' for 3 different API calls.

  • list.js : This function returns the top n items from the leaderboard where n can be set by query parameter count. If there is no query parameter n defaults to 10.
const redis = require("redis");

exports.handler = async (event) => {
    const client = redis.createClient({
        host: process.env.ENDPOINT,
        port: process.env.PORT,
        password: process.env.PASSWORD
    });

    const {promisify} = require('util');
    const zrevrangeAsync = promisify(client.zrevrange).bind(client);

    if (event.httpMethod !== 'GET') {
        throw new Error(`list only accept GET method, you tried: ${event.httpMethod}`);
    }
    console.info('list event received:', event);
    const count = event['queryStringParameters'] && event['queryStringParameters']['count'] ? event['queryStringParameters']['count'] : 10
    let result = await zrevrangeAsync("leaderboard", 0, count, "WITHSCORES");

    let response = {
        statusCode: 200,
        headers: {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Headers": "Content-Type",
            "Access-Control-Allow-Methods": "OPTIONS,GET"
        },
        body: JSON.stringify(result)
    };
    return response;
}
  • rank.js : This function returns the rank of the item where the id is passed as a query parameter id.
const redis = require("redis");

exports.handler = async (event) => {
    const client = redis.createClient({
        host: process.env.ENDPOINT,
        port: process.env.PORT,
        password: process.env.PASSWORD
    });

    const {promisify} = require('util');
    const zrevrankAsync = promisify(client.zrevrank).bind(client);

    if (event.httpMethod !== 'GET') {
        throw new Error(`rank only accept GET method, you tried: ${event.httpMethod}`);
    }
    console.info('received:', event);
    client.on("error", function (err) {
        throw err;
    });

    const id = event['queryStringParameters'] ? event['queryStringParameters']['id'] : null
    const rank = await zrevrankAsync("leaderboard", id);
    return {
        statusCode: 200,
        headers: {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Headers": "Content-Type",
            "Access-Control-Allow-Methods": "OPTIONS,GET"
        },
        body: JSON.stringify({"id": id, "rank" : rank })
    };
}
  • submit.js : This function receives {id, score} with a POST request and puts it into the leaderboard. It returns the rank of the item.
const redis = require("redis");

exports.handler = async (event) => {
    const client = redis.createClient({
        host: process.env.ENDPOINT,
        port: process.env.PORT,
        password: process.env.PASSWORD
    });

    const {promisify} = require('util');
    const zaddAsync = promisify(client.zadd).bind(client);
    const zrevrankAsync = promisify(client.zrevrank).bind(client);

    if (event.httpMethod !== 'POST') {
        throw new Error(`postMethod only accepts POST method, you tried: ${event.httpMethod} method.`);
    }
    console.info('submit event received:', event);

    // Get id and score from the body of the request
    const body = JSON.parse(event.body)
    const id = body["id"];
    const score = body["score"];

    client.on("error", function (err) {
        throw err;
    });

    await zaddAsync("leaderboard", score, id);
    const rank = await zrevrankAsync("leaderboard", id);

    return {
        statusCode: 200,
        headers: {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Headers": "Content-Type",
            "Access-Control-Allow-Methods": "OPTIONS,POST"
        },
        body: JSON.stringify({"id": id, "rank": rank})
    };

}

Redis database

For our application to work, we need to create a Redis database from Lambda Store console. Free tier should be enough. It is pretty straight forward but if you need help, check here The Lambda functions read the Redis server's endpoint and password from the environment variables. We will see how to set environment in the following section.

Deployment Template

To deploy our functions to AWS, we need to have deployment template file (template.yml) where we configure the functions and the API Gateway endpoint. See the deployment file. AWS SAM creates the required resources and deploy the Lambda functions using this file. You should set the environment variables for your Redis database. You can find the endpoint, port and password from Lambda Store console and set them in the template file as below:
env

Deployment

Now we are ready to deploy our application. First build the application via sam build. Then run the command sam deploy --guided. Enter a stack name and pick your region. After confirming changes, the deployment should begin.
You can check your deployment on your AWS console. You should have 3 functions created:
functions

Click one of your functions, you will see the code is uploaded and API Gateway is configured.

function

Testing

We can test our functions using the API Gateway endpoints. You can use curl or Postman. First you need to find the API endpoint by clicking on API Gateway. Let's add a new score to the leaderboard. You need to replace your API endpoint of your submit function in the below:

curl -d '{"id":"user2", "score": 1}' -H 'Content-Type: application/json' YOUR_ENDPOINT_FOR_SUBMIT_FUNCTION

After adding a few scores, you can see the leaderboard in your browser with the API endpoint of your list function.
lb

What is next?

We have implemented a basic leaderboard example. You may probably need to improve this in a real world scenario. For example you can store details about user using in Redis Hash. Currently the score is being sent from the client side. You may prefer to calculate it on the server side. If you are interested to see a more complete application, have a look at my blog where I implemented a game with the serverless stack.

Looking forward to hearing your feedback.

Posted on by:

svenanderson profile

Sven Anderson

@svenanderson

Making the world serverless

Lambda Store

Lambda Store is the first the `serverless Redis` service. In this blog, Lambda Store engineering team shares their experiences on Cloud, AWS, Kubernetes, Redis and of course Lambda Store.

Discussion

markdown guide