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 (0)