8 - How I hacked a clap button on my blog

Very important update:

I’ve decided to shut down this feature, simply because of the monthly RDS bill from Amazon. This blog is not generating any income to sustain the $200ish yearly fee for hosting an RDS instance for the claps, and the money has to come out of my own pocket, so in the name of saving money, I’ve shut down the clap button on my blog for now, although as I am writing these very words, it just occured to me that I can re-use the functionality and use a Google Sheets as a database. Cool idea, but until I get to it, there will be no clap button on the good old blog.

The article is still really interesting though, you should read it.

End of very important update.

Oh hey there, long time no see. It’s Easter Weekend here in the UK and I used up the free time to work on finishing up some tasks. One of these tasks was finishing my previous blog post on Python Decorators. I’m really happy with how that turned out, and I am even happier, because I sent it to my friend Ines and she really liked it too. After she read it, she asked me why doesn’t my blog have a “like” or “share to Facebook” button. The latter one is not really that hard to implement, I think it’s literally copy pasting some HTML and you’re good to go.

But the Like button? Now this was a feisty one.

My blog is a statically generated site, meaning that it has no back end running, and it doesn’t have a database to read and write to. My blog posts are literally markdown files, that live in the base directory of my site, that get converted to HTML. I like it that way. I wanted a very simple writing experience, and I wanted my readers (basically just Andy) to have a simple reading experience - no forms to submit, no newsletters to sign up to, just good old content to read through and learn something from.

But I started thinking about how I could implement a Like button on my site, and the more and more I thought about it, the more I started liking the idea of the challenge. So here’s how I hacked together a clap button on my blog

Big boi disclaimer

  • First of all, you should know that I have not followed best practices. Huge emphasis on “hacking” a Like button
  • I spend a couple of days on implementing this from an idea to reality. So I have cut some corners, and maybe slipped up an anti-pattern here and there
  • This post’s idea is for me to show you the Full Stack experience this took to implement, as it covered a lot of the stack - AWS, databases, Python, React and CSS.

Ok, so now that we have the excuse for my shitty code disclaimer out of the way, let’s dig in!

The plan

First of all, let’s think how we want our button to behave. Let’s put our Product Manager hat and write some requirements:

  • Each blog post should have a Like button at the end of the post
  • The button should show the reader the current number of likes
  • When the reader clicks the button, it increments the likes, and they can immediately see the new number of likes.
  • A reader can click the button as many times as they like, there is not a constrict between N likes and a reader

Ok, so me the code monkey has just been handed the above requirements from me, the product manager. Let’s start thinking how can we implement each step.

Each blog post should have a Like button at the end of the post

Right from the get-go, I am thinking about a storage solution. We have 2 use cases that require us to maintain the state of the like button - each button has to be tied to an article, and each button’s likes should represent the amount of likes for that article. We need a place to store some information about the article, and the number of likes it has

The button should show the reader the current number of likes

Just like above, we need a place to retrieve and modify the number of likes.

When the reader clicks the button, it increments the likes, and they can immediately see the new number of likes.

Ok, so now it gets interesting. We need to be able to interact with our storage solution somehow. Considering, that our site is statically generated, and the button is a client side element, we could start thinking about the button making an API call to retrieve and increment likes.

A reader can click the button as many times as they like, there is not a constrict between N likes and a reader

Our Product Manager has been cool with us. We don’t need to worry about extra logic to tie sessions to the buttons, plus someone can sit down literally click the button 1500000 times, and we’ll end up feeling good about ourselves. Well, they could also write a loop that does it and take down our storage solution, but the PM told us not to worry about it, as we have a userbase of approximately 3 people, and all 3 of them are nice.

Technical implementation - storage solution

We need a place to store data about our articles and the amount of likes they’ve received.

There are a lot of free storage solutions out there, but because (spoiler alert) I will be using AWS Lambda to implement the API, I decided that I’ll deploy a free tier mySQL RDS instance and store my data there. I logged in to my AWS console, clicked on RDS, and created a dev/test version of a mySQL DB. This part is pretty straightforward, so I won’t go into the steps it took. It’s just clicks of a button.

I logged in to my instance from my terminal like so:

mysql -u fake_username -p'fake_password' -h fake-db-name.rds.amazonaws.com -P 3306

And then I ran:

create database claps;
use claps;
create table claps_count(clap_id int(6) auto_increment primary key, blog_title varchar(30), claps_count int(6));

And that’s it. I now had the schema, that I needed to store my likes. I should mention here, that I went Medium style and in stead of making a Like button, I made a Clap button. Damn, I miss Medium when it wasn’t full of shit.

Technical implementation - creating an API

This is the meat and potatoes of the our button. The API call, which will allow our button to retrieve and increment likes for our articles.

Enter AWS Lambda.

AWS Lambda put in the simplest way possible is a “Function that lives in the cloud”. I love AWS Lambda, and I have used it for many little projects - I’ve built 2 Slack bots, that we use every day at work, and I used it to write some chat commands for my friend’s Twitch channel. It’s really satisfying to use and you can go as simple or complicated as you want with it. Let’s see how we can create a Lambda, that we’ll use as our API.

Let’s open up our AWS console and go to the Lambda section from the Services menu. Click on create function, and you’ll be greeted with this screen.

tweets

I chose Python 3.8 as my runtime environment, selected “Author from scratch” and created my function.

Now let’s add a trigger to invoke our function. Since we’ll be using an API call, we’ll use AWS’s API Gateway to trigger our function. The API Gateway will also provide is with an URL, which will be the API endpoint. Simply click on “Add trigger” like so:

tweets

Nice! Now it’s time to do a very simple config on our trigger:

tweets

I didn’t mess with the settings too much, the only thing I adjusted was the CORS settings to allow my blog to communicate with my API’s resources.

Writing the Lambda code

AWS gives you a little IDE, which you can use to develop your function, and it serves well for tiny projects, where you don’t need to use any dependencies. But since I needed a database driver for my Python code, I had to take another approach.

If you want to add dependencies to your lambda, you have to create a virtual environment in a folder, install your dependencies locally and then pack up your folder as a zip file, which you’ll upload to your lambda function, replacing all of the previous code you had in there with each upload. Let’s see how we can do this in Python.

First, I simply just made a virtualenv:

virtualenv claps-package

Then I activated it:

source virtualenv claps-package/bin/activate

My DB driver of choice is pymysql - it has a pretty easy to use API and for a simple CRUD it does an amazing job.

So here’s where I hit my first pain point. I installed pymysql with pip inside my virtualenv, and when I uploaded my code to AWS, it kept throwing, because it couldn’t find pymysql. It drove me up the wall!

After searching through Stack Overflow, I learned, that you have to tell pip to explicitly install your requirements in the same directory like so:

pip3 install pymysql --target .

I just saved you a little bit of temporary insanity. You’re welcome :)

Let’s create a little DBManager class to help us separate the database logic from our lambda handler logic.

I made a folder, called services, with a single db_manager.py file inside, where I wrote my logic:

Let’s start with the most important thing for our service - handling our database connection.

import os
import pymysql

class DBManager(object):
  
  def __init__(self):
    pass

  def get_db_connection(self):
    connection = pymysql.connect(
            host=os.environ.get('DB_HOST'),
            user=os.environ.get('DB_USER'),
            password=os.environ.get('DB_PASSWORD'),
            database=os.environ.get('DB_NAME'),
            port=int(os.environ.get('DB_PORT')),
            cursorclass=pymysql.cursors.DictCursor
        )
    return connection

I extracted the connection in a separate function from the constructor, because I wanted to have it on demand, rather than keeping it open when I instantiate the service. This allows us to not worry about closed connections, or about connections that stay open for too long. Need a connection? There you go, ask me again next time. Nice and easy.

Now let’s write our next method - retrieving article and claps data from the database:

def get_blog_post(self, blog_title):
    connection = self.get_db_connection()
    result = None
    with connection:
      with connection.cursor() as cursor:
        cursor.execute(f"select * from claps_count where blog_title = '{blog_title}';")
        result = cursor.fetchone()
        if result is None:
          cursor.execute(f"insert into claps_count(blog_title, claps_count) values('{blog_title}', 0);")
          connection.commit()
          return self.get_blog_post(blog_title)
        
    return result

Ok, so here we have our first anti-pattern, and let me explain myself.

This function is going to be used everytime we GET to our API. A GET should, as it says GET resources, and if you’re eagled-eyed, you have seen, that this function performs a create too. If I don’t get a result for the blog_title from the database, I recursivelly call the same method again and create one.

The reasons I took this approach are:

  • I did not want to spend time manually adding the blog titles to my database
  • Remember that my site is statically generated, I don’t have an easy way to implement automatically adding new post titles to the database, everytime I make a new post.

Nice, let’s write a a method to retrieve tha number of claps an article has

def get_claps_count(self, blog_title):
    blog_post = self.get_blog_post(blog_title)
    return blog_post.get('claps_count')

pymysql.cursors.DictCursor gives us the SQL data in a dictionary format, which allows us to easily do a .get. Nothing fancy here.

All that is left to do now is creating a function to increment our likes:

def increment_claps(self, blog_title):
    connection = self.get_db_connection()
    blog_post = self.get_blog_post(blog_title)
    blog_post_id = blog_post.get('clap_id')
    claps_count = blog_post.get('claps_count')
    with connection:
      with connection.cursor() as cursor:
        cursor.execute(f"update claps_count set claps_count = {claps_count + 1} where clap_id = {blog_post_id}")
      connection.commit()
    return self.get_claps_count(blog_title)

Nothing crazy here either, using our get_blog_post method to retrieve a row, then simply incrementing it by one and returning the claps count.

Now that we have our service code ready, let’s write our lambda handler in our lamba_function.py file:

import json
from services.db_manager import DBManager
 
def lambda_handler(event, context):
    db_manager = DBManager()
    method = event.get('requestContext').get('http').get('method')
    claps = None
    if method == 'POST':
        body = event.get('body')
        blog_title = json.loads(body).get('title')
        try:
            claps = db_manager.increment_claps(blog_title)
        except Exception:
            claps = str(Exception)
    else:
        blog_title = event.get('queryStringParameters').get('title')
        try:
            claps = db_manager.get_claps_count(blog_title)
        except Exception as e:
            claps = str(e)
    return {
        'statusCode': 200,
        'body': json.dumps({'claps': claps})
    }

Nothing crazy here either.

  • We instantiate our DBManager
  • Get the method from the request context
  • If it’s a GET, we call the db_manager.get_claps_count with the query string parameter, called title from the request and we return the claps
  • If it’s a POST, we call the db_manager.increment_claps with the data from the request body, and again we return the claps.

Nice! We are all set to deploy this bad boy on AWS Lambda!

Let’s package up our code in a zip file like so:

venv-pack --prefix claps-package --output claps.zip --format zip

And we simply hit the upload zip button on AWS Lambda!

The Front End

My blog is built with React, and oh boy, has it been ages since I wrote some, so here’s my outdated class style component that I wrote:

import React from "react"

class ClapButton extends React.Component {
  constructor(props) {
    super(props)
    this.state = {claps: 0}
    this.addClap = this.addClap.bind(this);
  }
  componentDidMount() {
    this.fetchClaps()
  }
  fetchClaps() {
    fetch(`https://p2rxf8866f.execute-api.eu-west-1.amazonaws.com/default/clap-function?title=${this.props.title}`)
    .then(response => response.json())
    .then(data => this.setState({claps: data.claps}))
  }
  addClap() {
    const {title} = this.props;
    fetch('https://p2rxf8866f.execute-api.eu-west-1.amazonaws.com/default/clap-function', {
      method: 'POST',
      body: JSON.stringify({'title': title})
    })
    .then(response => response.json())
    .then(data => this.setState({claps: data.claps}))
  }
  render() {
    return (
      <button className="clap-button" data-claps={this.state.claps} onClick={this.addClap}>👏</button>
    )
  }
}

export default ClapButton

Whenever the component loads, I want it to fetch the results for the current blog’s title, which I get from the props, and whenever it’s clicked, we send a POST to increment the likes. Standard stuff.

Let’s give it a little cosmetic makeover:

.clap-button {
  background: none;
  outline: none;
  border: none;
  border-radius: 50%;
  padding: 20px;
  box-shadow: 0 1px 12px -2px #777;
  cursor: pointer;
  transition: all ease-in-out 50ms;
  margin-bottom: 20px;
  font-size: 25px;
  position: relative;
}

.clap-button:active {
  transform: scale(1.1);
  box-shadow: 0 1px 12px 0px #41d8ca;
}

.clap-button:after {
  content: attr(data-claps);
  position: absolute;
  font-size: 12px;
  width: 20px;
  left: 50%;
  margin-left: -10px;
  bottom: 7px;
}

And we simply include it in our blog post template component like so:

<ClapButton title={location.pathname}/>

And this is it! We made it! We now have a nice little button we hacked together in a couple of days, which Andy can now click when he’s reading my articles!

Conclusion

This was a super fun little project to hack together, and even though it’s seemingly just a tiny button, it did take some planning and thinking to become reality.

I hope it helped you learn about statically generated sites, and how to deploy functions on AWS that can do some pretty cool stuff. I didn’t need to refactor my site, and I added some extra functionality to it, without having to deploy a server, or a database.

Until next time, and oh… don’t forget to hit that clap button 8-)


Written by Emil Mladenov - a slavic software developer who decided to use a blog as a digital rubber duck

I also have a podcast