Sign Up

Sign Up to our social questions and Answers Engine to ask questions, answer people’s questions, and connect with other people.

Have an account? Sign In

Have an account? Sign In Now

Sign In

Login to our social questions & Answers Engine to ask questions answer people’s questions & connect with other people.

Sign Up Here

Forgot Password?

Don't have account, Sign Up Here

Forgot Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Have an account? Sign In Now

Sorry, you do not have permission to ask a question, You must login to ask a question.

Forgot Password?

Need An Account, Sign Up Here

Please type your username.

Please type your E-Mail.

Please choose an appropriate title for the post.

Please choose the appropriate section so your post can be easily searched.

Please choose suitable Keywords Ex: post, video.

Browse

Need An Account, Sign Up Here

Please briefly explain why you feel this question should be reported.

Please briefly explain why you feel this answer should be reported.

Please briefly explain why you feel this user should be reported.

Sign InSign Up

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Logo

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Navigation

  • Home
  • About Us
  • Contact Us
Search
Ask A Question

Mobile menu

Close
Ask a Question
  • Home
  • About Us
  • Contact Us
Home/ Questions/Q 5908

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise Latest Questions

Author
  • 60k
Author
Asked: November 27, 20242024-11-27T01:38:12+00:00 2024-11-27T01:38:12+00:00

Add a Likes counter to your website with Cloudflare workers

  • 60k

So you have spent time and money to build a beautiful-looking website for your product or maybe a fancy portfolio website to impress your potential clients cool! You also added Google Analytics to track traffic but you can’t tell how many people liked it based on analytics data. 😟

Well, what if I show you a simple way to track it? And the answer is simple: just add a like button ♥️. We see those everywhere on every platform today since Facebook introduced it to the world.

But why serverless solution or Cloudflare worker to be specific? Good question. Yes, we can do it the regular way with an API from a dedicated Nodejs or python web server with MongoDB approach but it will be an overkill to develop & deploy a server with a database just to keep track of like counts. (at least in my case)

Our goal here is to:

  • to store the Likes count at some global storage so that we can read the updated value anytime,
  • increase the Likes count when a user clicks our like button and decrease the Likes count when they click it again,
  • Learn how to develop and deploy an API in minutes (with Wrangler-cli)

Assumptions / Prerequisite :

  • An active Cloudflare account
  • any code editor
  • Node version > 16 installed
  • Some knowledge about React and Rest APIs
  • Basic javascript/Typescript skills

Before, jumping into the coding part first let’s try to break down the process and define some steps to implement the feature. Let’s start with the API part we need three endpoints for our use case

  1. /getTotalLikes: to get a current count of likes
  2. /incrementLikes: to increase Likes count by 1 when the user clicks the Like button
  3. /decrementLikes: to decrease Likes count by 1 when the user clicks the Like button again (unlike)

Once we have these three endpoints, we can hook them up to the react button component and also display the latest Likes count around it.

We have a small plan now.

Let’s begin building:

Step 1: Create a hello word Cloudflare worker project using the below command

npm create cloudflare@latest  
Enter fullscreen mode Exit fullscreen mode

It will walk you through boilerplate setup. Feel free to pick any configuration you like.

My project config :

Typescript: Always, Yes.

Deploy: Yes. It will open a live URL in the browser. (easy to deploy)

Whoami: login to Cloudflare account via browser.

Untitled.png

Step 2: Creating a KV Namespace

KV stands for key-value storage it works like a browser local storage but it’s not stored on the client’s browser but rather close to the browser in the cdns. Read more about it here.

Run the following command :

npx wrangler kv:namespace create LIKES 
Enter fullscreen mode Exit fullscreen mode

Untitled.png

Next, copy over values from the terminal to the wrangler.toml file of your project

kv_namespaces = [     { binding = "<YOUR_BINDING>", id = "<YOUR_ID>" } ] 
Enter fullscreen mode Exit fullscreen mode

Screenshot of wrangler.toml file after adding kv namespace binding

Think of namespaces as different collections of a redis-cache, it can store. up to 10,000 documents with a max size of 25MB for each value. Read, more about the limits of KV Namespaces here.

Now, we are ready to use KV namespace inside our worker. The KV Namespace provides two methods, to put and to get the data.

1.put(key:string, value:string|ReadableStream|ArrayBuffer):Promise<void>

This method returns a Promise that you should await on in order to verify a successful update.

  1. get(key:string):Promise<string|ReadableStream|ArrayBuffer|null>

The method returns a promise you can await on to get the value. If the key is not found, the promise will resolve with the literal value null.

```bash export default {   async fetch(request, env, ctx) {         await env.NAMESPACE.put("first-key", "1")     const value = await env.NAMESPACE.get("first-key");      if (value === null) {       return new Response("Value not found", { status: 404 });     }     return new Response(value);   }, }; ``` 
Enter fullscreen mode Exit fullscreen mode

or we can use cli command to set the initial key-value pair 
Enter fullscreen mode Exit fullscreen mode

```bash npx wrangler kv:key put --binding=LIKES "likes_count" "0" ``` 
Enter fullscreen mode Exit fullscreen mode

**In the src/worker.ts file**  1. uncomment line 13 and rename `MY_NAMESPACE` with `LIKES` 2. Inside the fetch function on line 30 add the above code to get value from the store.  As you can see we can leverage typescript to be confident about KV Namespace during runtime    ![worker.ts: accessing ‘LIKE’ KVNamespace inside the worker](https://s3.us-west-2.amazonaws.com/secure.notion-static.com/7974c0a4-ba94-4286-afad-79aa2cd583b5/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20230901%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20230901T090007Z&X-Amz-Expires=3600&X-Amz-Signature=728b173f5719def159624798310b7201a940124eff1662bd8a9747d60617b55f&X-Amz-SignedHeaders=host&x-id=GetObject)   Let’s deploy this worker by running the following command: 
Enter fullscreen mode Exit fullscreen mode

```bash npx wrangler deploy ``` 
Enter fullscreen mode Exit fullscreen mode

Untitled.png

Open the URL in your browser

Untitled.png

I think you must have understood what’s happening here and how we can interact with KV storage.

Step 3: Handle API endpoints

Let’s circle back to our requirements we need an endpoint /getLikesCount which will return updated likes_count from the KV store.

We can leverage new URL () standard URL object to get pathname

URL object pattern diagram

Quickly copy and paste this code into the worker.ts file

const requestUrl = new URL(request.url); const { hostname, pathname, search, hash } = requestUrl; 
Enter fullscreen mode Exit fullscreen mode

Accessing URL object values inside the worker

You can try console logging hostname, pathname, search, hash to understand URL object

But, we are interested in the pathname we can check if it matches substring /getLikes

likes count,

if (pathname.includes('getLikes')) {             return new Response('pathname includes substring `getLikes`');     } 
Enter fullscreen mode Exit fullscreen mode

Using substring to match pathname and send custom response

Let’s deploy and test our worker now by visiting /getLikes path

Screenshot of chrome tab accessing worker https address

Great! It’s working we can now respond to different paths with different responses.

Let’s build on this and add two more paths /incrementLikes and /decrementLikes.

Also, let’s use another key likes_count to track count.

Run this command to create a new key-value pair and initialize with 0.

npx wrangler kv:key put --binding=LIKES "likes_count" "0" 
Enter fullscreen mode Exit fullscreen mode

Add the following code to handle the other pathnames

if (pathname.includes('incrementLikes')) {             return new Response('pathname includes substring `incrementLikes`');         }         if (pathname.includes('decrementLikes')) {             return new Response('pathname includes substring `incrementLikes`');         } 
Enter fullscreen mode Exit fullscreen mode

Now, we can do simple addition and subtraction to the likes_count key and return the appropriate value.

Also, since we want to consume it as API let’s send it as JSON response. I’m leaving the implementation up to you. Notice, that I’ve also added an Access-Control-Allow-Origin header in the response so that we can call this API from any domain in the browser. (to prevent CROS errors)

const likesCount = await env.LIKES.get('likes_count');         const requestUrl = new URL(request.url);         const { hostname, pathname, search, hash } = requestUrl;          //handle `/getLikes` request and responded with likesCount         if (pathname.includes('getLikes')) {             const json = JSON.stringify({ status: 200, message: `count at ${new Date().getTime()}`, likesCount }, null, 2);             return new Response(json, {                 headers: {                     'content-type': 'application/json;charset=UTF-8',                     'Access-Control-Allow-Origin': '*',                 },             });         }          //handle `/incrementLikes` request and responded with updated likesCount         if (pathname.includes('incrementLikes')) {             let updatedCount = parseInt(likesCount || '7') + 1;             let status = 200;             let message = `count updated at ${new Date().getTime()}`;             try {                 await env.LIKES.put('likes_count', updatedCount.toFixed(0));             } catch (error) {                 console.error('Error in incrementing likes', error);                 if (likesCount) {                     updatedCount = parseInt(likesCount);                 }                 status = 500;                 message = `failed to update count error: ${JSON.stringify(error)}`;             }             const json = JSON.stringify({ status, message, likesCount: updatedCount }, null, 2);             return new Response(json, {                 headers: {                     'content-type': 'application/json;charset=UTF-8',                     'Access-Control-Allow-Origin': '*',                 },             });         }          //handle `/decrementLikes` request and responded with updated likesCount         if (pathname.includes('decrementLikes')) {             let updatedCount = parseInt(likesCount || '7') - 1;             let status = 200;             let message = `count updated at ${new Date().getTime()}`;              try {                 await env.LIKES.put('likes_count', updatedCount.toFixed(0));             } catch (error) {                 console.error('Error in decrementing likes', error);                 if (likesCount) {                     updatedCount = parseInt(likesCount);                 }                 status = 500;                 message = `failed to update count error: ${JSON.stringify(error)}`;             }             const json = JSON.stringify({ status, message, likesCount: updatedCount }, null, 2);             return new Response(json, {                 headers: {                     'content-type': 'application/json;charset=UTF-8',                     'Access-Control-Allow-Origin': '*',                 },             });         }          //handle '*' requests         const json = JSON.stringify({ status: 404, message: `unknown/missing path` }, null, 2);         return new Response(json, {             headers: {                 'content-type': 'application/json;charset=UTF-8',                 'Access-Control-Allow-Origin': '*',             },         }); 
Enter fullscreen mode Exit fullscreen mode

Step 4: Test the API endpoint

Let’s quickly deploy and test our worker endpoints

Screenshot of chrome browser accessing /getLikes endpoint

Screenshot of chrome browser incrementing likes by hitting /incrementLikes endpoint

You can reload the page multiple times to increment the likes’ counter

Screenshot of chrome browser incrementing likes by hitting /incrementLikes endpoint multiple times

Similarly, we should also check the current likes count again by calling /getLikes endpoint

Screenshot of browser checking likes count with /getLikes endpoint after incrementing likes

Let’s test the decrement counter also,

Screenshot of browser hitting /decrementLikes endpoint

Let’s verify by visiting /getLikes endpoint

Screenshot of verifying like count by hitting /getLikes endpoint after decrementing likes count

Congratulations 🎉, your API endpoints are ready! (Look how easy it was, right?) 😄

Now, all you gotta do is integrate them into your client app. I’m using a React app for my portfolio website so I’ll be showing you the implementation for the same.

Step 5: Integrating API into our React app

This part is quite straightforward, we need a Like button component which on clicking will call our CF worker /API endpoint and update the likes count on UI.

Here’s a simple component with Heart Icon and Count Label. Here’s the link to the GitHub page.

import EmptyHeart from "./Icons/EmptyHeart"; import FilledHeart from "./Icons/FilledHeart";  const LikeButton: React.FC<{   liked: boolean;   likesCount: number;   onLike: (e: any) => void; }> = ({ liked, likesCount, onLike }) => {   return (     <div className="flex gap-2 items-center justify-center p-1">       {!liked ? (         <span           className="animate-pulse cursor-pointer select-none"           onClick={onLike}         >           <EmptyHeart />         </span>       ) : (         <span           className="cursor-pointer select-none animate-bounce"           onClick={onLike}         >           <FilledHeart />         </span>       )}       <h6         className={`italic text-white font-medium ${           liked ? "animate-[pulse_2s_ease-in_forwards]" : ""         }`}       >         {likesCount}       </h6>     </div>   ); }; 
Enter fullscreen mode Exit fullscreen mode

I created a custom hook useLikeButton to abstract the API logic on how I call the API and update the state, you can see the source code here.

const useLikeButton = () => {   const [isLiked, setIsLiked] = useState(false);   const [likesCount, setLikesCount] = useState(0);    useEffect(() => {     (async () => {       const data = await getLikesCount();       if (data && data.status === 200) setLikesCount(parseInt(data.likesCount));     })();   }, [isLiked]);    const handleLikeButtonClick = async () => {     setIsLiked(!isLiked);     try {       if (isLiked) {         const data = await decrementLike();         if (data && data.status === 200) {           console.log("decrement success");           setLikesCount(parseInt(data.likesCount));         }       } else {         const data = await incrementLike();         if (data && data.status === 200) {           console.log("increment success");           setLikesCount(parseInt(data.likesCount));         }       }     } catch (err) {       //roll back       if (isLiked) setLikesCount((prev) => prev + 1);       else setLikesCount((prev) => prev - 1);     }   };    return {     isLiked,     likesCount,     handleLikeButtonClick,   }; }; 
Enter fullscreen mode Exit fullscreen mode

incrementLike and decrementLike functions look like this again check the source code for more details.

We only need a GET request here because we are not sending any payload to the worker but you can create POST request too with CloudFlare workers.

import { ADD_LIKE, ADD_UNLIKE, GET_LIKES_COUNT } from "../constants";  export const GET = async (url: string) => {   try {     const response = await (await fetch(url)).json();     return response;   } catch (err) {     console.error("Oops! Failed to get likes", err);   } };  export const getLikesCount = () => GET(GET_LIKES_COUNT); export const incrementLike = () => GET(ADD_LIKE); export const decrementLike = () => GET(ADD_UNLIKE); 
Enter fullscreen mode Exit fullscreen mode

Finally, I added the LikeButton component to the page and passed the necessary props to it from our custom hook.

const { isLiked, likesCount, handleLikeButtonClick } = useLikeButton();  return( ... //above code hidden for brevity  <LikeButton           liked={isLiked}           likesCount={likesCount}           onLike={handleLikeButtonClick}         />  ) 
Enter fullscreen mode Exit fullscreen mode

That is it! We should now be able to see it in action.

There’s one problem though, since we don’t have user identity attached to our likes count data we cannot tell if the user has already liked our page. This could result in the same user liking it multiple times because the button resets every time the page is refreshed or revisted.

Like button without localstorage

As you can tell, this happens because our state gets reset inside React app.

Step 6: Persistence problem on the client side

One simple way to fix this issue is to use the browser’s local storage.

We will set a flag (’has-like’) in local storage and based on that update the button state.

So, whenever a user visits our page we first check if that particular key is present or not

  • If the flag is present we keep the button state as liked and when the user clicks the button we decrement it and change the state to unlike.
  • if it's not, we keep the button state as unlike, and when the user clicks the button we increment it and change the state to like.

Step 7: Adding Local storage to persist state locally

I’ll use this simple custom hook useLocalStorage to set/get local storage data, the signature will be very similar to React’s useState hook and it will be interchangeable.

type SetValue<T> = Dispatch<SetStateAction<T>>; export function useLocalStorage<T>(   key: string,   initialValue: T ): [T, SetValue<T>] {   const readValue = useCallback((): T => {     if (typeof window === "undefined") {       return initialValue;     }      try {       const item = window.localStorage.getItem(key);       return item ? (parseJSON(item) as T) : initialValue;     } catch (error) {       console.warn(`Error reading localStorage key “${key}”:`, error);       return initialValue;     }   }, [initialValue, key]);    // State to store our value   // Pass initial state function to useState so logic is only executed once   const [storedValue, setStoredValue] = useState<T>(readValue);    const setValue: SetValue<T> = (value) => {     // Prevent build error "window is undefined" but keeps working     if (typeof window === "undefined") {       console.warn(         `Tried setting localStorage key “${key}” even though environment is not a client`       );     }     try {       // Allow value to be a function so we have the same API as useState       const newValue = value instanceof Function ? value(storedValue) : value;       window.localStorage.setItem(key, JSON.stringify(newValue));       setStoredValue(newValue);     } catch (error) {       console.warn(`Error setting localStorage key “${key}”:`, error);     }   };    useEffect(() => {     setStoredValue(readValue());     // eslint-disable-next-line react-hooks/exhaustive-deps   }, []);    return [storedValue, setValue]; }  // A wrapper for "JSON.parse()"" to support "undefined" value function parseJSON<T>(value: string | null): T | undefined {   try {     return value === "undefined" ? undefined : JSON.parse(value ?? "");   } catch {     console.log("parsing error on", { value });     return undefined;   } } 
Enter fullscreen mode Exit fullscreen mode

One last step, We just need to replace useState inside our custom hook with this local storage hook

Before

Untitled.png

After

Untitled.png

Time to test. Let’s refresh the page and click the like button then refresh the page again.

Voila, the button is still in a like state.

Like button with localstorage

Phew! That seemed like a lot of work but trust me we saved tons of time in API development for simple use cases like this.

Another thing to note here is that Vercel also provides similar KV storage so we can implement the same APIs using Vercel Edge Functions also.

Let me know in the comments if you would like me to write a blog on that as well or just comment on how you liked this one.

Our Learnings from this blog post:

  • How to create an API endpoint using CloudFlare worker, ✅
  • How to use key-value storage for data persistence in our APIs ✅
  • how to save cost by not booting up an EC2 instance for simple APIs. ✅ 🤑

Please do visit my website and drop a like. This post was published using a cross-platform publishing tool Buzpen.

reactserverlesstypescriptwebdev
  • 0 0 Answers
  • 0 Views
  • 0 Followers
  • 0
Share
  • Facebook
  • Report

Leave an answer
Cancel reply

You must login to add an answer.

Forgot Password?

Need An Account, Sign Up Here

Sidebar

Ask A Question

Stats

  • Questions 4k
  • Answers 0
  • Best Answers 0
  • Users 2k
  • Popular
  • Answers
  • Author

    How to ensure that all the routes on my Symfony ...

    • 0 Answers
  • Author

    Insights into Forms in Flask

    • 0 Answers
  • Author

    Kick Start Your Next Project With Holo Theme

    • 0 Answers

Top Members

Samantha Carter

Samantha Carter

  • 0 Questions
  • 20 Points
Begginer
Ella Lewis

Ella Lewis

  • 0 Questions
  • 20 Points
Begginer
Isaac Anderson

Isaac Anderson

  • 0 Questions
  • 20 Points
Begginer

Explore

  • Home
  • Add group
  • Groups page
  • Communities
  • Questions
    • New Questions
    • Trending Questions
    • Must read Questions
    • Hot Questions
  • Polls
  • Tags
  • Badges
  • Users
  • Help

Footer

Querify Question Shop: Explore Expert Solutions and Unique Q&A Merchandise

Querify Question Shop: Explore, ask, and connect. Join our vibrant Q&A community today!

About Us

  • About Us
  • Contact Us
  • All Users

Legal Stuff

  • Terms of Use
  • Privacy Policy
  • Cookie Policy

Help

  • Knowledge Base
  • Support

Follow

© 2022 Querify Question. All Rights Reserved

Insert/edit link

Enter the destination URL

Or link to existing content

    No search term specified. Showing recent items. Search or use up and down arrow keys to select an item.