Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/santiagonieto09/portafolio/llms.txt

Use this file to discover all available pages before exploring further.

src/infrastructure/github/github-api.server.ts is the sole boundary between the portfolio and the GitHub REST API. Every outbound request to api.github.com, every piece of raw API data, and every mapping to domain types lives in this one server-only file. Nothing outside the infrastructure layer ever sees a raw GitHubUser or GitHubRepo — only the mapped domain types cross the layer boundary.

HTTP client

The internal get<T>(path, attempts) helper is the single entry point for all GitHub REST calls. Base URL: https://api.github.com Authentication: The headers() function reads GITHUB_TOKEN (falling back to GITHUB_API_KEY) from the process environment. When either is set, every request carries Authorization: Bearer <token>, raising the rate limit from 60 to 5 000 requests per hour. When neither is set the request is sent unauthenticated. Required headers sent on every request:
HeaderValue
Acceptapplication/vnd.github+json
X-GitHub-Api-Version2022-11-28
User-Agentportfolio-site
Retry logic: get retries up to attempts times (default 3). Retryable conditions are:
  • HTTP 403 — often a secondary rate-limit response rather than an auth error.
  • HTTP 429 — explicit rate limit exceeded.
  • HTTP 5xx — transient GitHub server error.
  • Network exception — connection reset, DNS failure, etc.
Retries use exponential backoff: 500 * 2 ** attempt ms sleep after each failed attempt. With the default of attempts = 3 there are at most three tries, separated by sleeps of 500 ms and 1 000 ms. Any other 4xx response (404, 422, etc.) is treated as a permanent client error and returns null immediately without retrying.

Parallel fetch strategy

fetchPortfolio(username) fires four API requests simultaneously to minimise total latency:
const [user, reposRaw, socialRaw, eventsRaw] = await Promise.all([
  get<GitHubUser>(`/users/${username}`),
  get<GitHubRepo[]>(`/users/${username}/repos?per_page=100&sort=updated`),
  get<GitHubSocialAccount[]>(`/users/${username}/social_accounts`),
  get<GitHubEvent[]>(`/users/${username}/events/public?per_page=30`),
]);
  • Repos: fetches up to 100 repositories sorted by last-updated date. Forked repositories are filtered out before any further processing.
  • Events: up to 30 public events are fetched; the first 8 are mapped to ActivityItem records.
  • Social accounts: supplemental links (LinkedIn, Instagram, etc.) merged with the profile’s blog and twitter_username fields.
After the four parallel calls resolve, per-repository language and release data are fetched with controlled concurrency. The mapLimit helper runs at most CONCURRENCY = 3 in-flight requests at once, which avoids triggering GitHub’s secondary rate limits that kick in when many parallel requests hit the same API resource in quick succession.

Repository enrichment

buildRepository(raw) enriches each raw GitHubRepo record with two additional API calls:
const [languagesRaw, releaseRaw] = await Promise.all([
  get<GitHubLanguages>(`/repos/${raw.full_name}/languages`, 1),
  get<GitHubRelease>(`/repos/${raw.full_name}/releases/latest`, 1),
]);
Both calls use attempts = 1 (fail-fast). Because up to 100 repositories are processed, a slow retry chain on each would make the total fetch time unacceptably long. Instead, any failure gracefully degrades:
  • Languages: falls back to { [raw.language]: 1 } using the primary language field from the list endpoint, or an empty object if the primary language is also null.
  • Release: set to null on failure; the UI simply omits the release badge for that repository.
After the language map is resolved, detectTechnologies() (from the domain layer) infers the technology stack from the repository name, description, topics, language list, and homepage URL, returning an array of technology slugs (e.g. "react", "spring-boot", "docker"). The fully enriched record is mapped to the domain Repository type — all raw GitHub field names (stargazers_count, html_url, etc.) are normalised to camelCase domain names (stars, htmlUrl, etc.) at this boundary.
Forked repositories are excluded from the portfolio. The raw repository list is filtered with rawRepos.filter(r => !r.fork) before any enrichment or mapping. Only public, non-fork repositories appear in the portfolio UI.

Degraded mode

The portfolio is designed to render correctly even when GitHub returns no data. The fallback chain in fetchPortfolio is:
  1. Cache hit — fresh snapshot returned immediately, no GitHub calls needed.
  2. Successful fetch — user data received; repositories, activity, and socials are mapped and cached.
  3. No user data (e.g. rate limit exceeded on the /users/{username} call):
    • Calls readSnapshotOrStale() — returns the last known snapshot from memory even if its 7-day TTL has expired.
    • If no stale snapshot exists, calls degradedSnapshot(username) which returns a structurally valid PortfolioSnapshot with degraded: true, an empty repository list, and a minimal profile built from the username alone.
The UI renders normally in all three cases. Components check the degraded flag to optionally display a banner, but no error boundary is triggered and the page always produces valid HTML.

URL sanitisation

Social links and repository homepages pass through safeExternalUrl() before being stored in the domain types. The function:
  1. Detects bare domains (no scheme) and prepends https://.
  2. Parses the result with the URL constructor to validate structure.
  3. Checks the protocol against an allowlist: only http: and https: are permitted.
  4. Returns null for any URL that fails parsing or uses a disallowed scheme (javascript:, data:, ftp:, etc.).
This means dangerous URLs from user-controlled GitHub fields (bio, homepage, social accounts) can never reach the browser as clickable links.

Build docs developers (and LLMs) love