I took the challenge to build a likes button into this blog. Since the site is compiled and then deployed as flat files, there is no backend or database to manage. From a security aspect, there is no safer way to develop a website, but it does add a bit of complexity to incorporate dynamic content.
My first attempt was to add Firebase as a dependency and wire it up to the likes button. This worked great as it gave me real-time updates across multiple browser sessions whenever I clicked the button. However, looking at the compiled, minified bundle, I noticed it had added over 220 KB!
With that in mind, I don’t think the trade-off for that much code for such a simple likes button makes any sense. This led me to explore other options and decided that cloud functions might be a great fit for this. I’ve seen coworkers use AWS lambda functions for various things, but I’ve never had to the opportunity to try them out myself. The thought of using cloud functions excited me since I get the benefits of an API server, without managing an API server.
Planning the API
The API is reasonably straightforward if you think roughly how the user interacts with a like button. Let’s break this down into user stories.
- As an anonymous user, I want to see the total likes count next to the likes button.
- As an anonymous user, when I click the likes button, it should increment the count by one.
Based on those two user stories, we can create two endpoints to satisfy the requirements. First, we need to fetch the current count for a specific post using a GET request. Secondly, update that count by one or create a new document using a PUT request.
Building the Cloud Function
Lets first start with some of the boilerplate. We’ll create a new directory and create an index file which can house our function. In the root folder of this project run the following commands:
Next, you’ll need to install some of the project’s dependencies. For this cloud function, I have chosen to install Express, Firebase admin and the Firebase functions packages by running the following
npm install command:
Alternatively, if you prefer yarn:
That takes care of the project’s dependencies and the now for actual function. Open up
index.js and insert the following boilerplate:
This is a bare-bones function and doesn’t do much at this point. We are importing a few required packages, configuring the Firebase connection, and then spinning up an Express server to handle each request. If you were to deploy this as is and make a request to the functions endpoint, you would get back an
OK message from Express.
Configuring the routes
GET a document
Starting with the GET request handler, let’s try and think for a second what this endpoint is going to do. A request from the client hits the Express server and then matches a specific route. The route needs to include the post ID to identify which document to query from the database. One caveat here is if the document doesn’t exist, we should return a default count instead of returning a 404 not found error.
We are using the Express routing parameters to match the ID. For those not familiar with Express routing,
:id is just a variable I defined to match any value included in the route. It then becomes accessible under the request object
The request comes in; we’ll look up a specific document in the likes collection using the ID. The Firebase API returns an
exists property we can use to check if the document was previously in the collection. If the document exists, return the data by calling
doc.data() or return a default value of zero.
PUT to create or update a document
Without knowing much about the Firebase API, some developers may make the mistake of fetching a document using the
get method and then calling
set to increment the value.
Instead of calling
get and then
set, fetching and updating a value or creating a new document altogether should be handled by using transactions. Transactions allow you to read the document and then update an existing value while guaranteeing that you are incrementing the latest value.
Let’s take this line by line since a lot is going on here. First, we start a transaction against the database and then get the current document by ID. Firebase returns an object containing two main properties, exists and data. If the document exists, we’ll increment the current count by one or return a default value of one. Again, if the document exists, we’ll have to call the transaction update method to update the existing value. If the document does not exist, call set instead.
Set vs update
Knowing when to call set over update is important since set overwrites the existing document entirely. Calling
update only updates the values you pass in. Take a look at the following example:
active: true would be removed entirely. You can see how this would be a problem if your object contained more than just
count. Instead, calling
update would only update the
count and leave
Now to test out this code in production! You need to have the gcloud CLI tools installed locally to run any of the deployment commands.
Once complete, you should get back a payload containing all the necessary information about your function. Look for
httpsTrigger.url, this is the endpoint you need to hit to invoke the function. In my case, the URL I get back is
Now to create a document, we can use a CURL request to hit the endpoint.
Furthermore, to return the count:
There you have it, a small single cloud function that acts as an API server to read and write post likes to a database. All without having to bloat the client and load the entirety of Firebase for such simple functionality.
This was my first time experimenting with cloud functions, and I think they have the potential to enhance the overall developer experience when it comes to creating easy CRUD operations or form submissions. I’ll be incorporating them into more and more side projects and experiment with the serverless framework in the future.
You can view the source of the function on Github.