Building a Scalable Article View Counter with Hono and Supabase

119 min read

In today's digital landscape, tracking article views is essential for understanding audience engagement and optimizing content strategies. In this blog post, we'll walk through how to create a scalable and efficient article view counter using the Hono web framework and Supabase as our backend service. By the end of this tutorial, you'll have a robust API endpoint that not only tracks views but also ensures data integrity by creating articles on the fly if they don't exist.

Table of Contents

  1. Introduction to Hono and Supabase
  2. Setting Up Your Project
  3. Configuring Supabase
  4. Creating the Hono Application
  5. Implementing the View Counter Logic
  6. Testing the API Endpoint
  7. Conclusion

Introduction to Hono and Supabase

What is Hono?

Hono is a lightweight, high-performance web framework for building serverless applications and APIs in TypeScript or JavaScript. It’s designed to be minimalistic yet powerful, making it an excellent choice for developers looking to create fast and efficient web services.

What is Supabase?

Supabase is an open-source Firebase alternative that offers a suite of backend services, including a PostgreSQL database, authentication, storage, and real-time subscriptions. It provides a seamless developer experience with easy integration into various frontend and backend technologies.

By combining Hono and Supabase, we can build a performant API that leverages Supabase's robust backend capabilities.


Setting Up Your Project

Before diving into the code, ensure you have the following prerequisites:

  • Node.js installed on your machine.
  • A Supabase account. If you don't have one, you can sign up for free.
  • TypeScript knowledge is beneficial but not mandatory.

Initialize Your Project

  1. Create a new directory for your project and navigate into it:

    mkdir hono-supabase-view-counter
    cd hono-supabase-view-counter
    
  2. Initialize a new Node.js project:

    npm init -y
    
  3. Install the necessary dependencies:

    npm install hono @supabase/supabase-js
    npm install -D typescript ts-node @types/node
    
  4. Initialize TypeScript:

    npx tsc --init
    

Configuring Supabase

Creating a Supabase Project

  1. Log in to your Supabase account and create a new project.
  2. Note down the Supabase URL and Anon Key from your project's settings. These credentials are essential for connecting your application to Supabase.

Setting Up the Database

We'll create a table named articles to store article data, including the view counts.

  1. Navigate to the SQL editor in your Supabase dashboard.

  2. Run the following SQL script to create the articles table:

    create table articles (
      id integer primary key,
      views integer not null default 0
    );
    
    -- Create a stored procedure to increment views
    create or replace function increment_view(row_id integer)
    returns integer as $$
    begin
      update articles set views = views + 1 where id = row_id;
      return (select views from articles where id = row_id);
    end;
    $$ language plpgsql;
    

This script creates the articles table with id and views columns and a stored procedure increment_view to handle view count increments atomically.


Creating the Hono Application

Let's dive into the core of our application. We'll set up the Hono server, connect to Supabase, and define the necessary routes.

Importing Dependencies

import { Hono } from "hono";
import { createClient } from "@supabase/supabase-js";
  • Hono: The web framework for handling HTTP requests.
  • Supabase Client: For interacting with the Supabase backend.

Initializing Hono

const app = new Hono();

This initializes a new Hono application instance.

Configuring Supabase

const supabaseUrl: string = "https://kayaaylwgggy.supabase.co";
const supabaseAnonKey: string =
  "eyJhbGciOiJIUzI14JKET2ZMyqaXffKmllyBfIFY";

const supabase = createClient(supabaseUrl, supabaseAnonKey);

Replace the supabaseUrl and supabaseAnonKey with your project's credentials. These keys allow your application to communicate securely with Supabase.


Implementing the View Counter Logic

The core functionality involves checking if an article exists, creating it if it doesn't, and then incrementing its view count.

The checkAndIncreaseViews Function

async function checkAndIncreaseViews(articleId: number): Promise<number> {
  // Check if the article exists
  let { data: article, error } = await supabase
    .from("articles")
    .select("id, views")
    .eq("id", articleId)
    .single();

  if (error && error.code !== "PGRST116") {
    throw new Error("Error checking article existence: " + error.message);
  }

  if (!article) {
    // Article doesn't exist, create it with default views = 0
    const { data: newArticle, error: insertError } = await supabase
      .from("articles")
      .insert({ id: articleId, views: 0 })
      .single();

    if (insertError) {
      throw new Error("Error creating article: " + insertError.message);
    }

    article = newArticle;
  }

  // Increment the view count using the stored procedure
  const { data, error: incrementError } = await supabase.rpc("increment_view", {
    row_id: articleId,
  });

  if (incrementError) {
    throw new Error("Error incrementing views: " + incrementError.message);
  }

  return data as number;
}

Breakdown:

  1. Check Article Existence:

    • Queries the articles table for the given articleId.
    • If an error occurs that's not related to the article not existing (PGRST116), it throws an error.
  2. Create Article if Not Found:

    • If the article doesn't exist, it inserts a new record with views initialized to 0.
    • Throws an error if the insertion fails.
  3. Increment View Count:

    • Calls the increment_view stored procedure to atomically increase the view count.
    • Returns the updated view count.

Defining the API Route

app.get("/article/view/:id", async (c) => {
  const articleId: number = parseInt(c.req.param("id"), 10);

  try {
    const newViews = await checkAndIncreaseViews(articleId);
    return c.json({
      message: "Views incremented successfully",
      views: newViews,
    });
  } catch (error) {
    return c.json({ error: (error as Error).message }, 400);
  }
});
  • Route: GET /article/view/:id
  • Functionality:
    • Extracts the id parameter from the URL.
    • Calls checkAndIncreaseViews to handle the logic.
    • Returns a JSON response with the updated view count or an error message.

Exporting the Application

export default app;

This allows the Hono application to be used by other modules or serverless functions.


Testing the API Endpoint

To ensure everything works as expected, let's test the API.

Running the Application

  1. Add a start script in your package.json:

    "scripts": {
      "start": "ts-node index.ts"
    }
    
  2. Create an index.ts file with the Hono application code.

  3. Start the server:

    npm start
    
  4. Test the endpoint using curl or any API testing tool like Postman.

    curl http://localhost:3000/article/view/1
    

    Expected Response:

    {
      "message": "Views incremented successfully",
      "views": 1
    }
    

    Each subsequent request will increment the views count accordingly.


Conclusion

In this tutorial, we've successfully built a robust API endpoint for tracking and incrementing article views using Hono and Supabase. This setup ensures scalability, data integrity, and ease of maintenance. By leveraging Supabase's powerful backend services and Hono's lightweight framework, developers can create efficient and performant applications with minimal overhead.

Future Enhancements

  • Authentication: Integrate Supabase Auth to restrict access to certain endpoints.
  • Rate Limiting: Prevent abuse by limiting the number of requests per IP or user.
  • Caching: Implement caching strategies to reduce database load for frequently accessed articles.
  • Analytics: Extend the functionality to track more metrics like unique views, user demographics, etc.

Feel free to customize and expand upon this foundation to suit your specific project needs. Happy coding!