A repository page shows useful details, but comparing projects or summarizing contributor activity often means gathering information from multiple API endpoints. In this tutorial, you’ll build a small JavaScript command-line analyzer that fetches public repository metadata and contributor data from the GitHub REST API. You’ll also validate input, follow pagination, handle rate limits and API errors, and keep any optional token out of frontend code.
Key takeaways
- Use the repository endpoint for project metadata and the contributors endpoint for paginated contributor data.
- Validate owner and repository input, then encode each path segment when constructing API URLs.
- Follow the API’s
Linkheader for pagination, and restrict pagination URLs to the expected GitHub API endpoint. - Check response status and rate-limit headers instead of assuming every request succeeds.
- Public data can be requested without a token. If you add authentication, keep the token in a server-side environment variable—not browser JavaScript.
What the analyzer will do
The project accepts an owner and repository name, such as octocat Hello-World. It requests the repository record and then collects contributor data, following the API’s pagination links. Finally, it prints a compact summary: description, primary language, stars, forks, open issues, and the most active contributors returned by the API.
The REST API returns structured JSON, so the program can select only the fields it needs. The repository endpoint is a good starting point; the contributors endpoint demonstrates why pagination matters. A response may contain only one page of results, not the complete collection.
This example is a Node.js command-line program, not a browser app. It uses unauthenticated requests for public data by default. A server-side GITHUB_TOKEN is optional and can provide authenticated API access, subject to the applicable GitHub limits and permissions. Never embed a token in client-side code, a public repository, or a URL.
Set up the project
Use Node.js 20 or newer so the example can use the built-in fetch API. Create a file named analyze-repo.mjs and run it with an owner and repository name:
node analyze-repo.mjs octocat Hello-World
To authenticate optional requests, set GITHUB_TOKEN in the environment where the script runs. For example, on macOS or Linux:
GITHUB_TOKEN=your_token node analyze-repo.mjs octocat Hello-World
For local development, use a secret manager or an untracked environment file loaded by your development setup. Do not commit credentials. The example does not require a token to retrieve public repository data.
Implement the analyzer
The script below validates its two arguments, uses a fixed API origin, checks HTTP responses, and follows contributor pagination links. It caps the number of pages so a large result set cannot make the sample run indefinitely. Increase or remove that cap only if your use case needs it and you have considered the request budget.
const API_ORIGIN = "https://api.github.com";
const API_VERSION = "2022-11-28";
const MAX_CONTRIBUTOR_PAGES = 10;
const [owner, repo] = process.argv.slice(2);
if (!owner || !repo || process.argv.length !== 4) {
console.error("Usage: node analyze-repo.mjs <owner> <repository>");
process.exit(1);
}
// This is input hygiene, not a substitute for GitHub's validation.
if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(owner)) {
console.error("Invalid owner name.");
process.exit(1);
}
if (!/^[A-Za-z0-9._-]{1,100}$/.test(repo)) {
console.error("Invalid repository name.");
process.exit(1);
}
const ownerPath = encodeURIComponent(owner);
const repoPath = encodeURIComponent(repo);
const repositoryPath = `/repos/${ownerPath}/${repoPath}`;
function requestHeaders() {
const headers = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": API_VERSION,
};
// This script runs on the server or in a trusted local environment.
// Never move this token into browser-side JavaScript.
if (process.env.GITHUB_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}
return headers;
}
async function getJson(url) {
const response = await fetch(url, {
headers: requestHeaders(),
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) {
let detail = "";
try {
const body = await response.json();
if (typeof body.message === "string") detail = body.message;
} catch {
// The error response may not contain JSON.
}
const remaining = response.headers.get("x-ratelimit-remaining");
const reset = response.headers.get("x-ratelimit-reset");
const retryAfter = response.headers.get("retry-after");
const limitInfo = [
remaining !== null ? `remaining=${remaining}` : null,
reset !== null ? `reset=${reset}` : null,
retryAfter !== null ? `retry-after=${retryAfter}` : null,
].filter(Boolean).join(", ");
const extra = [detail, limitInfo].filter(Boolean).join("; ");
throw new Error(`GitHub API returned HTTP ${response.status}${extra ? `: ${extra}` : ""}`);
}
return response;
}
async function getContributors() {
const contributors = [];
let nextUrl = new URL(`${repositoryPath}/contributors?per_page=100`, API_ORIGIN);
let pages = 0;
while (nextUrl && pages < MAX_CONTRIBUTOR_PAGES) {
pages += 1;
const response = await getJson(nextUrl);
const page = await response.json();
if (!Array.isArray(page)) {
throw new Error("Unexpected contributors response from GitHub.");
}
contributors.push(...page);
const linkHeader = response.headers.get("link") ?? "";
const nextMatch = linkHeader.match(/<([^>]+)>;\\s*rel="next"/);
if (!nextMatch) {
nextUrl = null;
continue;
}
const candidate = new URL(nextMatch[1]);
// Follow only GitHub API pagination links for this exact endpoint.
if (
candidate.protocol !== "https:" ||
candidate.origin !== API_ORIGIN ||
candidate.pathname !== `${repositoryPath}/contributors`
) {
throw new Error("GitHub returned an unexpected pagination URL.");
}
nextUrl = candidate;
}
return { contributors, pagesFetched: pages, truncated: nextUrl !== null };
}
async function main() {
const repositoryUrl = new URL(repositoryPath, API_ORIGIN);
const repositoryResponse = await getJson(repositoryUrl);
const repository = await repositoryResponse.json();
const { contributors, pagesFetched, truncated } = await getContributors();
const topContributors = [...contributors]
.sort((a, b) => b.contributions - a.contributions)
.slice(0, 5)
.map(({ login, contributions }) => ({ login, contributions }));
console.log(JSON.stringify({
fullName: repository.full_name,
description: repository.description,
language: repository.language,
stars: repository.stargazers_count,
forks: repository.forks_count,
openIssues: repository.open_issues_count,
defaultBranch: repository.default_branch,
contributorsFetched: contributors.length,
contributorPagesFetched: pagesFetched,
contributorResultsTruncated: truncated,
topContributors,
}, null, 2));
}
main().catch((error) => {
console.error(`Analysis failed: ${error.message}`);
process.exitCode = 1;
});
The owner and repository checks reject unexpected characters before constructing the request. encodeURIComponent adds a further boundary between user input and URL path syntax. The script still relies on GitHub to determine whether a repository exists and whether the caller can access it; local validation cannot establish that.
The API origin is fixed rather than taken from user input. Pagination links are checked for HTTPS, the expected origin, and the contributor endpoint path before being followed. These checks keep this example’s requests constrained to its intended API endpoint. They are not a general SSRF defense for an application that fetches arbitrary user-supplied URLs.
Understand pagination and the output
The contributors endpoint accepts per_page and page parameters and can return a Link response header with a rel="next" URL. The script reads that link rather than guessing that another page exists. It also limits requests to MAX_CONTRIBUTOR_PAGES; if the limit is reached while a next link remains, contributorResultsTruncated is true.
The result is a summary, not a complete representation of repository activity. In particular, open_issues_count can include open pull requests. Contributor attribution and counts are GitHub’s API data and should be presented as such, not treated as a complete measure of individual effort.
Handle rate limits and API errors
A successful HTTP response is not guaranteed. Invalid input can still refer to a repository that does not exist, and the API can return errors for inaccessible resources, exhausted limits, or transient failures. The sample checks response.ok before parsing a response as the expected data type. It reports the HTTP status and, when available, the API message and rate-limit headers.
For rate-limit responses, inspect the response headers rather than retrying immediately in a tight loop. GitHub documents primary rate-limit information in headers such as X-RateLimit-Remaining and X-RateLimit-Reset; a Retry-After header may also be relevant. A production application should distinguish rate limiting from other failures, respect any retry guidance, and use bounded retries with backoff only where appropriate. Do not retry invalid requests indefinitely.
If this analyzer becomes a web application, make API requests from a backend when authentication is needed. Store credentials in server-side configuration, apply least privilege, and avoid logging tokens. A public frontend may call the public API without a token, but it must still handle errors and rate limits. Do not expose a personal access token through a bundled environment variable or browser network request.
Common mistakes
- Assuming one page is the full result. A contributors response can be paginated. Follow the documented links or deliberately report that results are limited.
- Hard-coding a token in JavaScript. Frontend code is visible to users. Keep secrets on a trusted server or omit authentication for public data.
- Treating every
403as the same problem. Inspect the response body and headers; authorization and rate-limit situations need different handling. - Building a URL from unchecked input. Validate expected owner and repository formats, encode path segments, and keep the API host fixed.
- Trusting every pagination URL. When following response-provided links, verify they point to the expected HTTPS host and endpoint.
- Overstating what the metrics mean. Stars, forks, open issues, and contribution counts are API fields with specific definitions; they do not independently measure project quality or individual impact.
Conclusion
A useful GitHub repository analyzer does more than make a request: it validates inputs, handles paginated responses, reports API failures, and respects rate limits. This Node.js example provides a small foundation for those tasks while keeping optional credentials out of frontend code. Before extending it, check the current GitHub REST API documentation for endpoint behavior, headers, authentication, and rate-limit guidance.
