If you look at the bottom of this page, you’ll see a button that says something along the lines of “x people have liked this page”. Go ahead, click it and watch what happens!

This button calls a tiny Mojolicious microservice that I have created thats sole purpose is to keep track of how many people like the post. I’m going to quickly give an overview about how it works!

Mojolicious is a web application framework written in Perl. It is fantastic for building web applications of any size. I’m going to dive into the code that makes this whole process work. Before I begin, let me just be clear that ‘star’ and ‘like’ are synonymous with each other.

Process

Each post on my website has a star_id associated with it. This is simply a unique ID that is associated with each post. It is generated from Online UUID Generator. After clicking the button under the post, a request is sent to the microservice to increment the ‘star’ count. This is done by inserting a record into a SQLite database. In order to ensure that people can only like the post once, the IP address of the user who liked the content is taken into account. Don’t worry, your IP address is not stored in the database. Only the MD5 hash of it is.

Database Schema

CREATE TABLE IF NOT EXISTS stars (
 id INTEGER PRIMARY KEY,
 ip_address TEXT NOT NULL,
 star_id TEXT NOT NULL,
 CONSTRAINT unq UNIQUE (ip_address, star_id)
);"

As you can see, the stars table is very simple. It stores the star_id and the MD5 hash of the users IP address. There is a UNIQUE constraint on the table which ensures that each star_id and ip_address combination is unique.

Endpoints

my $r = $self->routes;

$r->get('/star/:star_id')->to('star#get_count');
$r->post('/star/:star_id')->to('star#increment');

This application only has two endpoints. One to increment the ‘star’ count (also known as the ‘like’ count) for each post, and one to simply view the number of likes. Mojolicious is known for its amazing websocket support, however I opted to using simple HTTP routes to limit the complexity of the application.


To get the current ‘like’ count for the post, we just need to get the number of rows in the stars table that match the passed in star_id.

sub _star_count {
    ( my $star_id ) = @_;

    my $dbh = DBI->connect( "dbi:SQLite:dbname=$db_name", "", "" );

    my $sth = $dbh->prepare("SELECT COUNT(*) as Total FROM stars WHERE star_id = ?;");
    $sth->execute($star_id);

    my $star_count = $sth->fetchrow_hashref->{Total};

    return $star_count;
}

To increment the ‘like’ count we just insert the request into the database, and let SQLite determine if this is a duplicate request.

sub increment {
    my $self    = shift;
    my $ip      = $self->req->headers->header('X-Real-IP') or $self->tx->remote_address;
    my $hash_ip = md5_hex($ip);
    my $star_id = $self->stash('star_id');

    my $dbh = DBI->connect( "dbi:SQLite:dbname=$db_name", "", "" );

    my $sth = $dbh->prepare(
        "INSERT INTO stars (ip_address, star_id) VALUES (?, ?)");

    $sth->execute( $hash_ip, $star_id );

    if ($sth->err) {
     # User has already liked this post
    }

    my $star_count = _star_count($star_id);

    $self->render(
        json => { ip => $hash_ip, star_id => $star_id, count => $star_count }
    );
}

Hooking it Up

Now that the backend is complete, we just have to tell the button on the page to call the appropriate routes. The entire front-end code is only ~40 lines long. Here it is!

window.onload = () => {
  const setButtonHTML = (button) => {
    button.innerHTML = "<div id='star-count'></div><div id='star-label'></div>";
  };

  const updateButtonCount = (button, count) => {
    button.style.display = 'block';
    button.querySelector('#star-count').innerHTML = count;
    if (count === 1) {
      button.querySelector('#star-label').innerHTML = "person has liked this page";
    } else {
      button.querySelector('#star-label').innerHTML = "people have liked this page";
    }
  };

  const STAR_SERVER_URL = window.STAR_SERVER_URL || 'http://localhost:3000'
  const starButtons = document.querySelectorAll('button.star');
  starButtons.forEach((starButton) => {
    const starID = starButton.dataset.starId;
    setButtonHTML(starButton);
    starButton.style.display = 'none';
    axios.get(`${STAR_SERVER_URL}/star/${starID}`).then((resp) => {
      updateButtonCount(starButton, resp.data.count);
    });
    starButton.addEventListener('click', () => {
      axios.post(`${STAR_SERVER_URL}/star/${starID}`).then((resp) => {
        updateButtonCount(starButton, resp.data.count);
      });
    });
    setInterval(() => {
      axios.get(`${STAR_SERVER_URL}/star/${starID}`).then((resp) => {
        updateButtonCount(starButton, resp.data.count);
      });
    }, 10000);
  });
};

This will display the button with the correct ‘like’ count, and it will increment the count when clicked. Additionally, the ‘like’ count is fetched every 10 seconds to get an accurate count without having to refresh the page.


That’s pretty much it. If you would like to see the full source code, please click here!