A TypeScript library to fetch GitHub user stats (contributions, languages, repo rankings) with optional React components.
npm install @nuwan-dev/github-statsbash
npm install @nuwan-dev/github-stats
`
---
π Quick Start
$3
You need a GitHub Personal Access Token to access GitHub's API:
1. Go to GitHub Settings β Tokens
2. Click "Generate new token (classic)" or create a Fine-grained token
3. Required scopes:
- β
read:user - For basic user information
- β
repo - Required for private repository access
Important: Without the repo scope, private repositories will not be included in language statistics.
4. Save your token securely in .env.local:
`bash
For server-side usage (Next.js Server Components, Node.js)
GITHUB_TOKEN=ghp_your_token_here
For client-side usage (Next.js Client Components)
NEXT_PUBLIC_GITHUB_TOKEN=ghp_your_token_here
`
Security Note: Never commit tokens to git. The .env.local file is automatically ignored by Next.js.
$3
`tsx
import {
ContributionGraph, // React component for graphs
fetchContributionCalendar, // API for contribution data
fetchLanguageStats, // API for language analysis
getTopLanguages, // Helper for top N languages
} from "@nuwan-dev/github-stats";
// For React components
import "@nuwan-dev/github-stats/style.css";
`
---
π Feature 1: Contribution Calendars
Beautiful, GitHub-style contribution graphs as React components.
$3
`tsx
import { ContributionGraph } from "@nuwan-dev/github-stats";
import "@nuwan-dev/github-stats/style.css";
export default function Page() {
return (
username="nuwandev"
githubToken={process.env.GITHUB_TOKEN}
/>
);
}
`
$3
`tsx
import {
fetchContributionCalendar,
ContributionGraph,
} from "@nuwan-dev/github-stats";
const calendar = await fetchContributionCalendar("nuwandev", token);
calendar={calendar}
yearLabel="2025"
totalLabel="Keep coding! π"
/>;
`
$3
`tsx
const lastQuarter = await fetchContributionCalendar(
"nuwandev",
token,
new Date("2024-10-01"),
new Date("2024-12-31"),
);
;
`
$3
#### Props
| Prop | Type | Required | Description |
| ------------- | ---------------------- | --------------- | ---------------------- |
| username | string | β
(auto-fetch) | GitHub username |
| githubToken | string | β
(auto-fetch) | GitHub token |
| calendar | ContributionCalendar | β
(manual) | Calendar data |
| yearLabel | string \| number | β | Custom header label |
| totalLabel | string | β | Custom footer text |
| className | string | β | Additional CSS classes |
#### fetchContributionCalendar()
`typescript
fetchContributionCalendar(
username: string,
token: string,
start?: Date, // Default: 1 year ago
end?: Date // Default: today
): Promise
`
Returns:
`typescript
interface ContributionCalendar {
weeks: ContributionWeek[];
total: number;
}
interface ContributionDay {
date: string; // ISO format: "2025-01-23"
count: number; // Number of contributions
level: number; // 0-4 (color intensity)
color?: string; // GitHub's color
}
`
---
π― Feature 2: Language Statistics
Analyze programming languages across ALL your GitHub repositories with complete accuracy.
$3
β
Complete Coverage - Fetches ALL repositories (public + private) and ALL languages (no 20-item limit)
β
Private Repository Support - Access private repos with proper token scopes
β
Flexible Filtering - Choose to include/exclude forks and private repos
β
Accurate Percentages - Based on total codebase size across all repos
$3
`typescript
import { fetchLanguageStats } from "@nuwan-dev/github-stats";
// Fetch all public repos, excluding forks (default)
const stats = await fetchLanguageStats("nuwandev", token);
console.log(stats);
// {
// languages: [
// {
// name: 'TypeScript',
// size: 1234567, // Bytes of code
// repoCount: 42, // Number of repos using this
// percentage: 45.2, // Percentage of total codebase
// color: '#3178c6' // GitHub's official color
// },
// { name: 'JavaScript', size: 987654, repoCount: 38, percentage: 36.1, ... },
// ...
// ],
// totalSize: 2734567, // Total bytes across all languages
// totalRepos: 45 // Total repositories analyzed
// }
`
$3
`typescript
// Fetch ALL repos including private ones
const allStats = await fetchLanguageStats("nuwandev", token, {
includePrivate: true, // Access private repos (requires 'repo' scope)
includeForks: false, // Still exclude forks
});
console.log(Analyzed ${allStats.totalRepos} repos (including private));
`
$3
`typescript
// Include forked repositories in analysis
const statsWithForks = await fetchLanguageStats("nuwandev", token, {
includePrivate: false,
includeForks: true, // Include forks
});
`
$3
`typescript
const completeStats = await fetchLanguageStats("nuwandev", token, {
includePrivate: true, // Default: false
includeForks: true, // Default: false
});
`
$3
`typescript
import { getTopLanguages } from "@nuwan-dev/github-stats";
// Get top 5 languages
const topSkills = await getTopLanguages("nuwandev", token, 5);
topSkills.forEach((skill, i) => {
console.log(${i + 1}. ${skill.name} - ${skill.percentage.toFixed(1)}%);
});
// 1. TypeScript - 45.2%
// 2. JavaScript - 36.1%
// 3. Python - 16.7%
// 4. Go - 1.5%
// 5. Rust - 0.5%
`
$3
`typescript
const stats = await fetchLanguageStats("username", token, {
includePrivate: true,
});
const profile = {
// Most used language
primarySkill: stats.languages[0],
// Expert level (>15% of codebase)
expertise: stats.languages.filter((l) => l.percentage > 15),
// Proficient (5-15%)
proficient: stats.languages.filter(
(l) => l.percentage >= 5 && l.percentage <= 15,
),
// Familiar (<5%)
familiar: stats.languages.filter((l) => l.percentage < 5),
// Metrics
diversity: stats.languages.length,
totalProjects: stats.totalRepos,
totalCodeSize: stats.totalSize,
};
`
$3
#### fetchLanguageStats()
`typescript
fetchLanguageStats(
username: string,
token: string,
options?: {
includePrivate?: boolean; // Default: false - Include private repos (requires 'repo' scope)
includeForks?: boolean; // Default: false - Include forked repos
}
): Promise
`
Returns:
`typescript
interface LanguageStatsResult {
languages: LanguageStats[]; // Sorted by percentage (high to low)
totalSize: number; // Total bytes across all languages
totalRepos: number; // Repositories analyzed
}
interface LanguageStats {
name: string; // "TypeScript", "JavaScript", etc.
size: number; // Total bytes of code
repoCount: number; // Number of repos using this language
percentage: number; // Percentage of total codebase (0-100)
color?: string; // GitHub's official color (#hex)
}
`
#### getTopLanguages()
`typescript
getTopLanguages(
username: string,
token: string,
limit?: number, // Default: 10
options?: {
includePrivate?: boolean;
includeForks?: boolean;
}
): Promise
`
---
π¨ Complete Examples
$3
`tsx
import {
fetchContributionCalendar,
fetchLanguageStats,
ContributionGraph,
} from "@nuwan-dev/github-stats";
import "@nuwan-dev/github-stats/style.css";
export default async function Portfolio() {
const username = "nuwandev";
const token = process.env.GITHUB_TOKEN!;
// Fetch all data in parallel
const [contributions, languages] = await Promise.all([
fetchContributionCalendar(username, token),
fetchLanguageStats(username, token, {
includePrivate: true,
includeForks: false,
}),
]);
return (
{/ Profile Header /}
@{username}
{contributions.total} contributions this year
Primary language: {languages.languages[0]?.name}
{/ Contribution Graph /}
calendar={contributions}
yearLabel={new Date().getFullYear()}
/>
{/ Top Skills /}
Top Skills
{languages.languages.slice(0, 5).map((lang) => (
{lang.name}
{lang.percentage.toFixed(1)}%
className="h-2 rounded-full"
style={{
width: ${lang.percentage}%,
backgroundColor: lang.color,
}}
/>
))}
{/ Stats Grid /}
Total Repos
{languages.totalRepos}
Languages
{languages.languages.length}
Code Size
{(languages.totalSize / 1024 / 1024).toFixed(2)} MB
);
}
`
$3
`tsx
"use client";
import { useEffect, useState } from "react";
import {
fetchLanguageStats,
ContributionGraph,
type LanguageStatsResult,
} from "@nuwan-dev/github-stats";
export default function StatsPage() {
const [languages, setLanguages] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadStats() {
const token = process.env.NEXT_PUBLIC_GITHUB_TOKEN!;
const stats = await fetchLanguageStats("nuwandev", token, {
includePrivate: true,
});
setLanguages(stats);
setLoading(false);
}
loadStats();
}, []);
if (loading) return Loading...;
return (
My GitHub Stats
{languages && (
{languages.languages.map((lang) => (
{lang.name}: {lang.percentage.toFixed(1)}%
))}
)}
);
}
`
export function SkillsChart({ username, token }) {
const [stats, setStats] = useState(null);
useEffect(() => {
fetchLanguageStats(username, token).then(setStats);
}, [username, token]);
if (!stats) return Loading skills...;
return (
Language Distribution
{/ Pie chart or bar chart /}
{stats.languages.map((lang) => (
className="w-4 h-4 rounded"
style={{ backgroundColor: lang.color }}
/>
{lang.language}
{lang.percentage.toFixed(1)}%
{lang.repos} repos
))}
Total: {stats.totalRepos} repositories, {stats.languages.length}{" "}
languages
);
}
``
$3
`typescript
// app/api/github-stats/route.ts
import {
fetchContributionCalendar,
fetchLanguageStats,
} from "@nuwan-dev/github-stats";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const username = searchParams.get("username");
if (!username) {
return NextResponse.json({ error: "Username required" }, { status: 400 });
}
try {
const token = process.env.GITHUB_TOKEN!;
const [contributions, languages] = await Promise.all([
fetchContributionCalendar(username, token),
fetchLanguageStats(username, token),
]);
return NextResponse.json({
username,
contributions: {
total: contributions.total,
weeks: contributions.weeks.length,
},
languages: {
primary: languages.languages[0],
top5: languages.languages.slice(0, 5),
total: languages.languages.length,
},
});
} catch (error) {
return NextResponse.json(
{ error: "Failed to fetch GitHub stats" },
{ status: 500 },
);
}
}
``
---
π― Framework Integration
$3
`tsx
// app/page.tsx
import "@nuwan-dev/github-stats/style.css";
import { ContributionGraph, fetchLanguageStats } from "@nuwan-dev/github-stats";
export default async function Page() {
const stats = await fetchLanguageStats("nuwandev", process.env.GITHUB_TOKEN!);
return (
username="nuwandev"
githubToken={process.env.GITHUB_TOKEN!}
/>
Primary Skills
{stats.languages.slice(0, 5).map((lang) => (
{lang.language}: {lang.percentage.toFixed(1)}%
))}
);
}
`
$3
`tsx
// pages/profile.tsx
import type { GetServerSideProps } from "next";
import {
fetchContributionCalendar,
fetchLanguageStats,
} from "@nuwan-dev/github-stats";
export const getServerSideProps: GetServerSideProps = async () => {
const token = process.env.GITHUB_TOKEN!;
const [contributions, languages] = await Promise.all([
fetchContributionCalendar("nuwandev", token),
fetchLanguageStats("nuwandev", token),
]);
return { props: { contributions, languages } };
};
export default function Profile({ contributions, languages }) {
return (
GitHub Profile
{contributions.total} contributions
Top language: {languages.languages[0]?.language}
);
}
`
$3
`tsx
// src/App.tsx
import { useEffect, useState } from "react";
import { fetchLanguageStats, ContributionGraph } from "@nuwan-dev/github-stats";
import "@nuwan-dev/github-stats/style.css";
export default function App() {
const [stats, setStats] = useState(null);
useEffect(() => {
fetchLanguageStats("nuwandev", import.meta.env.VITE_GITHUB_TOKEN)
.then(setStats)
.catch(console.error);
}, []);
return (
username="nuwandev"
githubToken={import.meta.env.VITE_GITHUB_TOKEN}
/>
{stats && (
Skills
{stats.languages.slice(0, 5).map((lang) => (
{lang.language}
))}
)}
);
}
`
---
π Security Best Practices
$3
- Store tokens in environment variables (.env.local)
- Use server-side rendering (Next.js App Router, SSR)
- Create API routes to proxy GitHub requests
- Use server-only tokens (GITHUB_TOKEN, not NEXT_PUBLIC_*)
$3
- Hardcode tokens in source code
- Expose tokens in client-side JavaScript
- Commit tokens to version control
- Use NEXT_PUBLIC_ prefix for GitHub tokens
$3
`typescript
// app/api/stats/route.ts - Server only
import { fetchLanguageStats } from "@nuwan-dev/github-stats";
export async function GET(request: Request) {
const token = process.env.GITHUB_TOKEN; // Server-side only
const { searchParams } = new URL(request.url);
const username = searchParams.get("username");
const stats = await fetchLanguageStats(username!, token!);
return Response.json(stats);
}
`
`tsx
// app/page.tsx - Client
"use client";
async function getStats(username: string) {
const res = await fetch(/api/stats?username=${username});
return res.json();
}
`
---
βοΈ Advanced Configuration
$3
The library uses GitHub's official color palette. To customize:
`css
/ Override in your global CSS /
.contribution-day[data-level="0"] {
background-color: #161b22;
}
.contribution-day[data-level="1"] {
background-color: #0e4429;
}
.contribution-day[data-level="2"] {
background-color: #006d32;
}
.contribution-day[data-level="3"] {
background-color: #26a641;
}
.contribution-day[data-level="4"] {
background-color: #39d353;
}
`
$3
Language stats don't change frequently. Consider caching:
`typescript
// Example with Redis
import { fetchLanguageStats } from "@nuwan-dev/github-stats";
import redis from "./redis-client";
async function getCachedLanguageStats(username: string, token: string) {
const cacheKey = github:lang:${username};
// Try cache first
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// Fetch fresh data
const stats = await fetchLanguageStats(username, token);
// Cache for 24 hours
await redis.setex(cacheKey, 3600 * 24, JSON.stringify(stats));
return stats;
}
`
$3
GitHub's GraphQL API has a 5,000 cost points per hour limit:
- Contribution calendar: ~1 point per request
- Language statistics: ~1-5 points per page (~3-5 pages for 300 repos)
You can safely fetch stats for hundreds of users per hour.
---
π§ Troubleshooting
$3
Ensure you're providing the required props:
`tsx
// β Wrong - missing token
// β
Correct - auto-fetch mode
// β
Correct - manual mode
`
$3
Import the CSS file:
`tsx
import "@nuwan-dev/github-stats/style.css";
`
$3
The user may:
- Not exist
- Have no public repositories
- Have only forked repositories (these are excluded)
- Have invalid GitHub token
$3
Make sure you're importing types:
`typescript
import type {
ContributionCalendar,
LanguageStats,
LanguageStatsResult,
} from "@nuwan-dev/github-stats";
`
---
π How It Works
$3
- Uses GitHub's GraphQL API
- Fetches contribution data for specified date range
- Maps contribution counts to color levels (0-4)
- Renders as React component with GitHub-style grid
$3
- Queries all user repositories (excludes forks)
- Fetches up to 20 languages per repository
- Aggregates by bytes of code (not repo count)
- Calculates percentages across total codebase
- Same algorithm as GitHub's profile language graph
Why bytes, not repo count?
- More accurate representation of actual work
- A 1M-line TypeScript project weighs more than a 10-line shell script
- Matches GitHub's official methodology
---
π TypeScript Support
Full type definitions included:
`typescript
import type {
// Contribution types
ContributionCalendar,
ContributionWeek,
ContributionDay,
ContributionGraphProps,
// Language types
LanguageStats,
LanguageStatsResult,
} from "@nuwan-dev/github-stats";
`
---
πΊοΈ Roadmap
- [ ] Organization statistics
- [ ] Pull request metrics
- [ ] Issue tracking analytics
- [ ] Commit frequency analysis
- [ ] Repository star history
- [ ] Follower/following insights
- [ ] Custom graph themes
- [ ] Export to PNG/SVG
---
π€ Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
1. Fork the repository
2. Create your feature branch (git checkout -b feature/amazing-feature)
3. Commit your changes (git commit -m 'Add amazing feature')
4. Push to the branch (git push origin feature/amazing-feature`)