rimubhai_
PortfolioBlogComponents

© 2026 rimubhai_ All rights reserved.

ContactPrivacy

GitHub Stars

A rate-limit protected component to display GitHub star counts with multi-level server proxying, 24-hour local caching, and instant fallback rendering.

facebook/react
vercel/next.js
tailwindlabs/tailwindcss
shadcn/ui
framer/motion

Interactive Studio

Test custom repositories and observe initial star behavior.

Quick try:

Prevents initial skeleton flicker during SSR or initial load.

Enter a repository path to preview

e.g., facebook/react or vercel/next.js

Rate Limit Proof

Cascades between Proxy, GitHub API, and Shields.io so it never breaks under load.

Zero-Wait Local Cache

Caches star counts in localStorage for 24h instant loads.

Token Acceleration

Supports GITHUB_TOKEN to unlock 5,000 req/hr capacity.

Radix & Tailwind

Built on Radix primitives, Lucide icons, and Tailwind utility classes.

Manual Installation Guide

01

Install Dependencies

Install Lucide icons and required Radix tooltip component.

pnpm add lucide-react @radix-ui/react-tooltip
02

Create API Proxy Route Handler

Create app/api/github-stars/route.ts to handle caching and rate limiting.

import { NextResponse } from "next/server"

// In-memory cache for serverless instance lifetime
const memoryCache = new Map<string, { stars: number; timestamp: number }>()
const CACHE_TTL = 3600 * 1000 // 1 hour

function parseShieldsStars(message: string): number | null {
  if (!message) return null
  const cleaned = message.trim().toLowerCase()
  if (cleaned.endsWith("k")) {
    const num = parseFloat(cleaned.slice(0, -1))
    return isNaN(num) ? null : Math.round(num * 1000)
  }
  if (cleaned.endsWith("m")) {
    const num = parseFloat(cleaned.slice(0, -1))
    return isNaN(num) ? null : Math.round(num * 1000000)
  }
  const parsed = parseInt(cleaned, 10)
  return isNaN(parsed) ? null : parsed
}

export async function GET(request: Request) {
  try {
    const { searchParams } = new URL(request.url)
    const repo = searchParams.get("repo")

    if (!repo || !repo.includes("/")) {
      return NextResponse.json(
        { error: "Invalid repository format" },
        { status: 400 }
      )
    }

    const cleanRepo = repo.trim().replace(/\s+/g, "")
    const cached = memoryCache.get(cleanRepo)

    if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
      return NextResponse.json(
        { stars: cached.stars, cached: true },
        {
          headers: {
            "Cache-Control":
              "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
          },
        }
      )
    }

    let stars: number | null = null

    // 1. Try official GitHub API (uses GITHUB_TOKEN if configured)
    try {
      const headers: Record<string, string> = {
        "User-Agent": "Rimu-GitHub-Stars-App",
        Accept: "application/vnd.github.v3+json",
      }
      const token =
        process.env.GITHUB_TOKEN || process.env.NEXT_PUBLIC_GITHUB_TOKEN
      if (token) {
        headers.Authorization = `Bearer ${token}`
      }

      const res = await fetch(`https://api.github.com/repos/${cleanRepo}`, {
        headers,
        next: { revalidate: 3600 },
      })

      if (res.ok) {
        const data = await res.json()
        if (typeof data.stargazers_count === "number") {
          stars = data.stargazers_count
        }
      }
    } catch {}

    // 2. Fallback to Shields.io API if rate limited or blocked
    if (stars === null) {
      try {
        const shieldsRes = await fetch(
          `https://img.shields.io/github/stars/${cleanRepo}.json`,
          { next: { revalidate: 3600 } }
        )
        if (shieldsRes.ok) {
          const shieldsData = await shieldsRes.json()
          stars = parseShieldsStars(shieldsData.message)
        }
      } catch {}
    }

    // 3. Fallback to stale memory cache if external endpoints failed
    if (stars === null && cached) {
      stars = cached.stars
    }

    if (stars !== null) {
      memoryCache.set(cleanRepo, { stars, timestamp: Date.now() })
      return NextResponse.json(
        { stars, cached: false },
        {
          headers: {
            "Cache-Control":
              "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
          },
        }
      )
    }

    return NextResponse.json(
      { stars: cached?.stars ?? 0, error: "Rate limit exceeded or repo unavailable" },
      { status: 200 }
    )
  } catch (error) {
    return NextResponse.json(
      { error: "Internal Server Error" },
      { status: 500 }
    )
  }
}
03

Create Component File

Create components/ui/github-stars.tsx.

"use client"

import { buttonVariants } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import { useEffect, useState } from "react"
import { Github } from "lucide-react"

type GitHubStarsProps = {
  repo: string
  initialStars?: number
  className?: string
}

function formatFullNumber(num: number) {
  return new Intl.NumberFormat("en-US").format(num)
}

function parseShieldsStars(message: string): number | null {
  if (!message) return null
  const cleaned = message.trim().toLowerCase()
  if (cleaned.endsWith("k")) {
    const num = parseFloat(cleaned.slice(0, -1))
    return isNaN(num) ? null : Math.round(num * 1000)
  }
  if (cleaned.endsWith("m")) {
    const num = parseFloat(cleaned.slice(0, -1))
    return isNaN(num) ? null : Math.round(num * 1000000)
  }
  const parsed = parseInt(cleaned, 10)
  return isNaN(parsed) ? null : parsed
}

export function GitHubStars({
  repo,
  initialStars = 0,
  className,
}: GitHubStarsProps) {
  const cleanRepo = repo ? repo.trim().replace(/s+/g, "") : ""
  const cacheKey = cleanRepo ? `gh_stars_v2_${cleanRepo}` : ""

  const [stars, setStars] = useState<number>(() => {
    if (typeof window === "undefined" || !cacheKey) return initialStars
    try {
      const cachedRaw = localStorage.getItem(cacheKey)
      if (cachedRaw) {
        const { stars: storedStars } = JSON.parse(cachedRaw)
        if (typeof storedStars === "number") return storedStars
      }
    } catch {}
    return initialStars
  })

  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState<boolean>(() => {
    if (initialStars > 0 || stars > 0) return false
    return true
  })

  useEffect(() => {
    let isMounted = true
    const controller = new AbortController()

    if (!cleanRepo || !cleanRepo.includes("/")) return

    // If cache is fresh (< 1 hour old), skip network request completely
    try {
      const cachedRaw = localStorage.getItem(cacheKey)
      if (cachedRaw) {
        const { timestamp } = JSON.parse(cachedRaw)
        if (timestamp && Date.now() - timestamp < 60 * 60 * 1000) {
          return
        }
      }
    } catch {}

    const fetchStars = async () => {
      try {
        let fetchedStars: number | null = null

        // Priority 1: Call internal proxy route
        try {
          const internalRes = await fetch(
            `/api/github-stars?repo=${encodeURIComponent(cleanRepo)}`,
            { signal: controller.signal }
          )
          if (internalRes.ok) {
            const data = await internalRes.json()
            if (typeof data.stars === "number" && data.stars >= 0) {
              fetchedStars = data.stars
            }
          }
        } catch {}

        // Priority 2: Direct GitHub API fallback
        if (fetchedStars === null) {
          try {
            const ghRes = await fetch(
              `https://api.github.com/repos/${cleanRepo}`,
              { signal: controller.signal }
            )
            if (ghRes.ok) {
              const ghData = await ghRes.json()
              if (typeof ghData.stargazers_count === "number") {
                fetchedStars = ghData.stargazers_count
              }
            }
          } catch {}
        }

        // Priority 3: Shields.io API fallback
        if (fetchedStars === null) {
          try {
            const shieldsRes = await fetch(
              `https://img.shields.io/github/stars/${cleanRepo}.json`,
              { signal: controller.signal }
            )
            if (shieldsRes.ok) {
              const shieldsData = await shieldsRes.json()
              fetchedStars = parseShieldsStars(shieldsData.message)
            }
          } catch {}
        }

        if (!isMounted) return

        if (fetchedStars !== null) {
          setStars(fetchedStars)
          setError(null)
          try {
            localStorage.setItem(
              cacheKey,
              JSON.stringify({ stars: fetchedStars, timestamp: Date.now() })
            )
          } catch {}
        } else if (stars === 0 && initialStars === 0) {
          setError("Unable to load star count")
        }
      } catch (err: any) {
        if (!isMounted || err.name === "AbortError") return
      } finally {
        if (isMounted) setLoading(false)
      }
    }

    fetchStars()

    return () => {
      isMounted = false
      controller.abort()
    }
  }, [cleanRepo, cacheKey, initialStars, stars])

  const formattedCompact = new Intl.NumberFormat("en-US", {
    notation: "compact",
    compactDisplay: "short",
    maximumFractionDigits: 1,
  })
    .format(stars)
    .toLowerCase()

  const cleanRepoDisplay = repo.replace(/\s+/g, "")

  return (
    <TooltipProvider delayDuration={150}>
      <Tooltip>
        <TooltipTrigger asChild>
          <a
            href={`https://github.com/${cleanRepoDisplay}`}
            target="_blank"
            rel="noopener noreferrer"
            aria-label={`Star ${cleanRepoDisplay} on GitHub`}
            className={cn(
              buttonVariants({ variant: "outline", size: "sm" }),
              "flex w-fit items-center gap-2 px-3 text-muted-foreground transition-colors hover:text-primary",
              className
            )}
          >
            <Github className="h-4 w-4 shrink-0" />

            <span
              className="mx-1 h-4 w-px shrink-0 bg-border"
              aria-hidden="true"
            />

            <span className="min-w-[2ch] text-[13px] tabular-nums">
              {loading && stars === 0 ? "—" : formattedCompact}
            </span>
          </a>
        </TooltipTrigger>
        <TooltipContent side="top" sideOffset={6} className="font-sans">
          <div className="flex flex-col gap-1 text-center">
            <span>{formatFullNumber(stars)} stars on GitHub</span>
            {error && <span className="text-xs text-destructive">{error}</span>}
          </div>
        </TooltipContent>
      </Tooltip>
    </TooltipProvider>
  )
}
04

Use in Your Application

Import and pass the repository prop.

import { GitHubStars } from "@/components/ui/github-stars"

export default function Page() {
  return <GitHubStars repo="vercel/next.js" />
}

Component Source Code

"use client"

import { buttonVariants } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import { useEffect, useState } from "react"
import { Github } from "lucide-react"

type GitHubStarsProps = {
  repo: string
  initialStars?: number
  className?: string
}

function formatFullNumber(num: number) {
  return new Intl.NumberFormat("en-US").format(num)
}

function parseShieldsStars(message: string): number | null {
  if (!message) return null
  const cleaned = message.trim().toLowerCase()
  if (cleaned.endsWith("k")) {
    const num = parseFloat(cleaned.slice(0, -1))
    return isNaN(num) ? null : Math.round(num * 1000)
  }
  if (cleaned.endsWith("m")) {
    const num = parseFloat(cleaned.slice(0, -1))
    return isNaN(num) ? null : Math.round(num * 1000000)
  }
  const parsed = parseInt(cleaned, 10)
  return isNaN(parsed) ? null : parsed
}

export function GitHubStars({
  repo,
  initialStars = 0,
  className,
}: GitHubStarsProps) {
  const cleanRepo = repo ? repo.trim().replace(/s+/g, "") : ""
  const cacheKey = cleanRepo ? `gh_stars_v2_${cleanRepo}` : ""

  const [stars, setStars] = useState<number>(() => {
    if (typeof window === "undefined" || !cacheKey) return initialStars
    try {
      const cachedRaw = localStorage.getItem(cacheKey)
      if (cachedRaw) {
        const { stars: storedStars } = JSON.parse(cachedRaw)
        if (typeof storedStars === "number") return storedStars
      }
    } catch {}
    return initialStars
  })

  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState<boolean>(() => {
    if (initialStars > 0 || stars > 0) return false
    return true
  })

  useEffect(() => {
    let isMounted = true
    const controller = new AbortController()

    if (!cleanRepo || !cleanRepo.includes("/")) return

    // If cache is fresh (< 1 hour old), skip network request completely
    try {
      const cachedRaw = localStorage.getItem(cacheKey)
      if (cachedRaw) {
        const { timestamp } = JSON.parse(cachedRaw)
        if (timestamp && Date.now() - timestamp < 60 * 60 * 1000) {
          return
        }
      }
    } catch {}

    const fetchStars = async () => {
      try {
        let fetchedStars: number | null = null

        // Priority 1: Call internal proxy route
        try {
          const internalRes = await fetch(
            `/api/github-stars?repo=${encodeURIComponent(cleanRepo)}`,
            { signal: controller.signal }
          )
          if (internalRes.ok) {
            const data = await internalRes.json()
            if (typeof data.stars === "number" && data.stars >= 0) {
              fetchedStars = data.stars
            }
          }
        } catch {}

        // Priority 2: Direct GitHub API fallback
        if (fetchedStars === null) {
          try {
            const ghRes = await fetch(
              `https://api.github.com/repos/${cleanRepo}`,
              { signal: controller.signal }
            )
            if (ghRes.ok) {
              const ghData = await ghRes.json()
              if (typeof ghData.stargazers_count === "number") {
                fetchedStars = ghData.stargazers_count
              }
            }
          } catch {}
        }

        // Priority 3: Shields.io API fallback
        if (fetchedStars === null) {
          try {
            const shieldsRes = await fetch(
              `https://img.shields.io/github/stars/${cleanRepo}.json`,
              { signal: controller.signal }
            )
            if (shieldsRes.ok) {
              const shieldsData = await shieldsRes.json()
              fetchedStars = parseShieldsStars(shieldsData.message)
            }
          } catch {}
        }

        if (!isMounted) return

        if (fetchedStars !== null) {
          setStars(fetchedStars)
          setError(null)
          try {
            localStorage.setItem(
              cacheKey,
              JSON.stringify({ stars: fetchedStars, timestamp: Date.now() })
            )
          } catch {}
        } else if (stars === 0 && initialStars === 0) {
          setError("Unable to load star count")
        }
      } catch (err: any) {
        if (!isMounted || err.name === "AbortError") return
      } finally {
        if (isMounted) setLoading(false)
      }
    }

    fetchStars()

    return () => {
      isMounted = false
      controller.abort()
    }
  }, [cleanRepo, cacheKey, initialStars, stars])

  const formattedCompact = new Intl.NumberFormat("en-US", {
    notation: "compact",
    compactDisplay: "short",
    maximumFractionDigits: 1,
  })
    .format(stars)
    .toLowerCase()

  const cleanRepoDisplay = repo.replace(/\s+/g, "")

  return (
    <TooltipProvider delayDuration={150}>
      <Tooltip>
        <TooltipTrigger asChild>
          <a
            href={`https://github.com/${cleanRepoDisplay}`}
            target="_blank"
            rel="noopener noreferrer"
            aria-label={`Star ${cleanRepoDisplay} on GitHub`}
            className={cn(
              buttonVariants({ variant: "outline", size: "sm" }),
              "flex w-fit items-center gap-2 px-3 text-muted-foreground transition-colors hover:text-primary",
              className
            )}
          >
            <Github className="h-4 w-4 shrink-0" />

            <span
              className="mx-1 h-4 w-px shrink-0 bg-border"
              aria-hidden="true"
            />

            <span className="min-w-[2ch] text-[13px] tabular-nums">
              {loading && stars === 0 ? "—" : formattedCompact}
            </span>
          </a>
        </TooltipTrigger>
        <TooltipContent side="top" sideOffset={6} className="font-sans">
          <div className="flex flex-col gap-1 text-center">
            <span>{formatFullNumber(stars)} stars on GitHub</span>
            {error && <span className="text-xs text-destructive">{error}</span>}
          </div>
        </TooltipContent>
      </Tooltip>
    </TooltipProvider>
  )
}

Props Reference

PropTypeDefaultDescription
repostringRequiredThe GitHub repository path in owner/repo format (e.g., vercel/next.js).
initialStarsnumber0Optional initial star count for immediate rendering during SSR or initial page load.
classNamestringundefinedAdditional Tailwind CSS utility classes to customize styling.

Usage Examples

basic
<GitHubStars repo="facebook/react" />
with Initial
<GitHubStars repo="vercel/next.js" initialStars={141000} />
custom Style
<GitHubStars repo="tailwindlabs/tailwindcss" className="border-primary/50 text-primary" />