DEV Community

Cover image for Got tired of GitHub stats widgets, so I built my own — OAuth, GraphQL, and a cache layer included
Adrish Dey
Adrish Dey

Posted on • Edited on

Got tired of GitHub stats widgets, so I built my own — OAuth, GraphQL, and a cache layer included

Your GitHub profile is a green grid and a list of repo names. It doesn't feel like yours.

So I built DevPrint — log in with GitHub, and it generates a card: your stats, top languages, contribution heatmap, and a shareable link. No forms. No manual input. Your identity, decoded from your own API data.

🔗 Live: devprint.adrish.me
💻 Code: github.com/adrish-mage/devprint

What it does

  • Log in with GitHub → your card generates instantly, no config
  • Language breakdown, follower/star counts, and a contribution heatmap — all computed server-side
  • Search any GitHub username and generate their card too, no login needed
  • Logged-in users get a permanent shareable link at /u/your-username
  • Zero third-party stats widgets. Every number on the card comes from raw GitHub API responses that I parse myself

Why build another GitHub card generator

Mostly because the existing ones are either static SVG badges or paywalled. I wanted to actually own the data pipeline — auth, caching, rate limits, all of it — rather than embed someone else's widget. It turned into a genuinely useful sandbox for a few things I hadn't touched before: OpenID Connect, GraphQL, and cache design under a real rate limit ceiling.

The stack

Layer Tech
Runtime Node.js 20
Server Express 4
Templating EJS
Auth Auth0 · GitHub OAuth (OIDC)
Data GitHub REST v3 + GraphQL v4
Persistence MongoDB (cache-aside, 1hr TTL)
Deploy Render + a Namecheap domain

A few technical bits worth mentioning

Parallel fetching, not sequential. Profile, repos, and the GraphQL contribution calendar all fire together:

const [profileRes, repoData, graphQL] = await Promise.all([
  axios.get(`https://api.github.com/users/${username}`, { headers }),
  axios.get(`https://api.github.com/users/${username}/repos`, { headers }),
  axios.post("https://api.github.com/graphql", buildQuery(username), { headers }),
]);
Enter fullscreen mode Exit fullscreen mode

Cache-aside with a TTL, not a cron job. No background refresh — every read checks freshness first, and only fetches from GitHub on a genuine miss:

async function getGithubData(username) {
  const cached = await Profile.findOne({ username });
  const isFresh = cached && (Date.now() - cached.fetchedAt) < ONE_HOUR;
  if (isFresh) return cached;
  const freshData = await fetchGitHubData(username);
  // ...upsert and return
}
Enter fullscreen mode Exit fullscreen mode

This is the difference between blowing through GitHub's 5000 req/hr ceiling on a traffic spike and not even noticing one.

GraphQL queries as variables, not string interpolation. Early version built the query with template literals directly around the username — worked fine functionally, but it's the kind of thing that looks bad in a code review and is bad practice regardless of actual exploitability. Fixed it to pass login as a proper GraphQL variable instead:

const buildQuery = (login) => ({
  query: `query($login: String!) { user(login: $login) { ... } }`,
  variables: { login }
});
Enter fullscreen mode Exit fullscreen mode

Failed searches don't dead-end. If you search a username that doesn't exist, you land back on your own card with an inline error — not a raw stack trace, not a blank page.

What's still rough

Being honest about it: no per-IP rate limiting yet (the cache absorbs most organic traffic, but a determined bad actor cycling random usernames could still force cache misses), and structured logging is still a roadmap item, not a shipped one. Both are next.

Try it

If you've got a GitHub account, your card is one login away: devprint.adrish.me

I'd genuinely like feedback — on the UI, on the code, on what's missing. Repo's open: github.com/adrish-mage/devprint. Do give a star if you like it ! Issues and PRs welcome.

Top comments (2)

Collapse
 
marcusykim profile image
Marcus Kim

The combination of parallel REST/GraphQL fetching and a one-hour cache-aside TTL is the right shape for a project whose usefulness depends on data freshness but whose provider has a hard request ceiling. I also like that the early string-interpolated GraphQL query was treated as a design smell and replaced with variables, even before it became an exploit; that kind of cleanup is how a sandbox turns into a service. The next founder-level question is whether permanent profile URLs should serve a slightly stale card gracefully when GitHub is unavailable, since predictable availability may matter more to sharers than perfectly current counts.

Collapse
 
rogue_mage profile image
Adrish Dey

Thanks for the thoughtful feedback. That's actually a great point about graceful degradation. Right now the cache helps reduce API dependency, but serving stale profile snapshots during GitHub downtime is something I would definitely like to explore.

Showing stats with a tag like "updated 2 hrs ago" for example, can acknowledge the situation yet serve the purpose to the people who are sharing it when GitHub is unavailable .
A stale card served gracefully is a reliable choice in my opinion.
For a permanent developer profile URL, reliability is definitely as important as freshness.