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 }),
]);
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
}
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 }
});
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)
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.
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.