DocsHub
JavascriptAdvanced

Fetch User Data

Build a user profile viewer that fetches data from a public API and displays it with loading, error, and empty states.

Fetch User Data

Problem

Build a GitHub profile viewer where the user types a username and the app fetches and displays their profile data from the GitHub API. Handle all possible states — loading, success, not found, and network error.

User types "torvalds" and clicks Search
→ Loading spinner appears
→ Data fetched from GitHub API
→ Profile card shown with avatar, name, bio, stats

User types "thisisnotarealuser999"
→ Loading spinner appears
→ API returns 404
→ "User not found" message shown

User has no internet
→ Loading spinner appears
→ Fetch fails
→ "Network error" message shown

User clears input and clicks Search
→ "Please enter a username" error shown

Logic

  1. Select the input, button, and display area
  2. On search — validate that the input is not empty
  3. Show loading state — hide previous results
  4. Fetch from https://api.github.com/users/{username}
  5. If response is 404 — show not found message
  6. If response is not ok — throw a general error
  7. Parse JSON and render the profile card
  8. Fetch their repositories separately for a repo count
  9. Catch any network errors and show an error state

Flow

yes no 404 other error 200 ok User clicks Search Input empty? Show validation error Show loading state Fetch from GitHub API Response status? Show not found message Show network error Parse JSON Render profile card Show success state

HTML Structure

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>GitHub Profile Viewer</title>
    <style>
      body {
        font-family: sans-serif;
        max-width: 520px;
        margin: 60px auto;
        padding: 0 20px;
      }

      h1 {
        margin-bottom: 6px;
      }

      p.subtitle {
        color: #888;
        font-size: 0.9rem;
        margin-bottom: 24px;
      }

      /* search row */
      .search-row {
        display: flex;
        gap: 10px;
        margin-bottom: 8px;
      }

      .search-row input {
        flex: 1;
        padding: 12px;
        font-size: 1rem;
        border: 2px solid #ccc;
        border-radius: 8px;
        outline: none;
        box-sizing: border-box;
      }

      .search-row input:focus {
        border-color: #555;
      }

      .search-row button {
        padding: 12px 20px;
        font-size: 1rem;
        font-weight: 600;
        background: #111;
        color: #fff;
        border: none;
        border-radius: 8px;
        cursor: pointer;
      }

      .search-row button:hover {
        opacity: 0.85;
      }

      .search-row button:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }

      /* validation error */
      #validation-error {
        color: #c0392b;
        font-size: 0.85rem;
        margin-bottom: 16px;
        min-height: 18px;
      }

      /* state containers */
      #loading,
      #error-state,
      #not-found,
      #profile {
        display: none;
      }

      /* loading */
      #loading {
        text-align: center;
        padding: 40px;
        color: #888;
      }

      .spinner {
        width: 36px;
        height: 36px;
        border: 3px solid #eee;
        border-top-color: #555;
        border-radius: 50%;
        animation: spin 0.8s linear infinite;
        margin: 0 auto 16px;
      }

      @keyframes spin {
        to { transform: rotate(360deg); }
      }

      /* error state */
      #error-state {
        background: #fff0f0;
        border-radius: 10px;
        padding: 24px;
        text-align: center;
        color: #c0392b;
      }

      #error-state h3 {
        margin: 0 0 8px;
      }

      #error-state p {
        margin: 0;
        font-size: 0.9rem;
      }

      /* not found */
      #not-found {
        background: #fff8e1;
        border-radius: 10px;
        padding: 24px;
        text-align: center;
        color: #856404;
      }

      #not-found h3 {
        margin: 0 0 8px;
      }

      #not-found p {
        margin: 0;
        font-size: 0.9rem;
      }

      /* profile card */
      #profile {
        border: 1px solid #e0e0e0;
        border-radius: 12px;
        overflow: hidden;
      }

      /* profile header with avatar */
      .profile-header {
        display: flex;
        align-items: center;
        gap: 20px;
        padding: 24px;
        border-bottom: 1px solid #f0f0f0;
      }

      .profile-avatar {
        width: 80px;
        height: 80px;
        border-radius: 50%;
        object-fit: cover;
      }

      .profile-names h2 {
        margin: 0 0 4px;
        font-size: 1.2rem;
        color: #111;
      }

      .profile-names .username {
        color: #888;
        font-size: 0.9rem;
      }

      /* bio section */
      .profile-bio {
        padding: 14px 24px;
        font-size: 0.9rem;
        color: #555;
        line-height: 1.5;
        border-bottom: 1px solid #f0f0f0;
      }

      /* stats row */
      .profile-stats {
        display: flex;
        border-bottom: 1px solid #f0f0f0;
      }

      .stat {
        flex: 1;
        text-align: center;
        padding: 14px;
        border-right: 1px solid #f0f0f0;
      }

      .stat:last-child {
        border-right: none;
      }

      .stat .value {
        font-size: 1.3rem;
        font-weight: 700;
        color: #111;
      }

      .stat .label {
        font-size: 0.75rem;
        color: #999;
        margin-top: 2px;
      }

      /* profile details */
      .profile-details {
        padding: 16px 24px;
        display: flex;
        flex-direction: column;
        gap: 8px;
      }

      .detail-row {
        display: flex;
        align-items: center;
        gap: 10px;
        font-size: 0.88rem;
        color: #555;
      }

      .detail-row .detail-icon {
        width: 16px;
        text-align: center;
        flex-shrink: 0;
      }

      /* view profile button */
      .profile-footer {
        padding: 14px 24px;
        border-top: 1px solid #f0f0f0;
      }

      .profile-footer a {
        display: block;
        text-align: center;
        padding: 10px;
        background: #111;
        color: #fff;
        border-radius: 8px;
        text-decoration: none;
        font-size: 0.9rem;
        font-weight: 600;
      }

      .profile-footer a:hover {
        opacity: 0.85;
      }
    </style>
  </head>
  <body>
    <h1>GitHub Profile Viewer</h1>
    <p class="subtitle">Search any GitHub username to view their profile.</p>

    <!-- search row -->
    <div class="search-row">
      <input
        type="text"
        id="username-input"
        placeholder="Enter GitHub username..."
      />
      <button id="search-btn">Search</button>
    </div>

    <!-- validation error -->
    <div id="validation-error"></div>

    <!-- loading state -->
    <div id="loading">
      <div class="spinner"></div>
      <p>Loading profile...</p>
    </div>

    <!-- error state -->
    <div id="error-state">
      <h3>⚠️ Something went wrong</h3>
      <p id="error-message">An unexpected error occurred.</p>
    </div>

    <!-- not found state -->
    <div id="not-found">
      <h3>🔍 User not found</h3>
      <p id="not-found-message">No GitHub user found with that username.</p>
    </div>

    <!-- profile card -->
    <div id="profile">
      <div class="profile-header">
        <img class="profile-avatar" id="avatar" src="" alt="Avatar" />
        <div class="profile-names">
          <h2 id="display-name"></h2>
          <div class="username" id="profile-username"></div>
        </div>
      </div>
      <div class="profile-bio" id="bio"></div>
      <div class="profile-stats">
        <div class="stat">
          <div class="value" id="stat-repos">-</div>
          <div class="label">Repos</div>
        </div>
        <div class="stat">
          <div class="value" id="stat-followers">-</div>
          <div class="label">Followers</div>
        </div>
        <div class="stat">
          <div class="value" id="stat-following">-</div>
          <div class="label">Following</div>
        </div>
      </div>
      <div class="profile-details">
        <div class="detail-row" id="detail-location" style="display:none">
          <span class="detail-icon">📍</span>
          <span id="location-text"></span>
        </div>
        <div class="detail-row" id="detail-company" style="display:none">
          <span class="detail-icon">🏢</span>
          <span id="company-text"></span>
        </div>
        <div class="detail-row" id="detail-blog" style="display:none">
          <span class="detail-icon">🔗</span>
          <a id="blog-link" href="" target="_blank" style="color:#3498db"></a>
        </div>
        <div class="detail-row">
          <span class="detail-icon">📅</span>
          <span id="joined-text"></span>
        </div>
      </div>
      <div class="profile-footer">
        <a id="github-link" href="" target="_blank">View on GitHub →</a>
      </div>
    </div>

    <script src="script.js"></script>
  </body>
</html>

Solution

// Step 1 — select elements
const usernameInput = document.querySelector("#username-input");
const searchBtn = document.querySelector("#search-btn");
const validationError = document.querySelector("#validation-error");

// all possible state panels
const loadingPanel = document.querySelector("#loading");
const errorPanel = document.querySelector("#error-state");
const notFoundPanel = document.querySelector("#not-found");
const profilePanel = document.querySelector("#profile");

// Step 2 — listen for search button click and Enter key
searchBtn.addEventListener("click", handleSearch);

usernameInput.addEventListener("keydown", (e) => {
  if (e.key === "Enter") handleSearch();
});

async function handleSearch() {
  const username = usernameInput.value.trim();

  // Step 3 — validate input
  if (!username) {
    validationError.textContent = "Please enter a username.";
    return;
  }

  validationError.textContent = "";

  // Step 4 — show loading, hide all other states
  showState("loading");
  searchBtn.disabled = true;

  try {
    // Step 5 — fetch user data from GitHub API
    const response = await fetch(`https://api.github.com/users/${username}`);

    // Step 6 — handle 404 specifically
    if (response.status === 404) {
      document.querySelector("#not-found-message").textContent =
        `No GitHub user found with username "${username}".`;
      showState("not-found");
      return;
    }

    // Step 7 — handle other non-ok responses
    if (!response.ok) {
      throw new Error(`GitHub API error: ${response.status}`);
    }

    // Step 8 — parse the JSON response
    const user = await response.json();

    // Step 9 — render the profile
    renderProfile(user);
    showState("profile");

  } catch (err) {
    // Step 10 — show network or unexpected errors
    document.querySelector("#error-message").textContent =
      err.message.includes("fetch")
        ? "Could not connect. Check your internet connection."
        : err.message;
    showState("error");
  } finally {
    // Step 11 — always re-enable the search button
    searchBtn.disabled = false;
  }
}

// Step 12 — render profile data into the DOM
function renderProfile(user) {
  // avatar and name
  document.querySelector("#avatar").src = user.avatar_url;
  document.querySelector("#avatar").alt = `${user.login}'s avatar`;
  document.querySelector("#display-name").textContent = user.name || user.login;
  document.querySelector("#profile-username").textContent = `@${user.login}`;

  // bio — hide section if no bio
  const bioEl = document.querySelector("#bio");
  if (user.bio) {
    bioEl.textContent = user.bio;
    bioEl.style.display = "block";
  } else {
    bioEl.style.display = "none";
  }

  // stats
  document.querySelector("#stat-repos").textContent = formatNumber(user.public_repos);
  document.querySelector("#stat-followers").textContent = formatNumber(user.followers);
  document.querySelector("#stat-following").textContent = formatNumber(user.following);

  // optional detail rows — only show if data exists
  showDetail("detail-location", "location-text", user.location);
  showDetail("detail-company", "company-text", user.company?.replace(/^@/, ""));

  // blog link
  if (user.blog) {
    document.querySelector("#detail-blog").style.display = "flex";
    const blogLink = document.querySelector("#blog-link");
    const url = user.blog.startsWith("http") ? user.blog : `https://${user.blog}`;
    blogLink.href = url;
    blogLink.textContent = user.blog;
  } else {
    document.querySelector("#detail-blog").style.display = "none";
  }

  // joined date
  const joined = new Date(user.created_at);
  document.querySelector("#joined-text").textContent =
    `Joined ${joined.toLocaleDateString("en-US", { month: "long", year: "numeric" })}`;

  // github link
  document.querySelector("#github-link").href = user.html_url;
}

// helper — shows or hides a detail row based on whether data exists
function showDetail(rowId, textId, value) {
  const row = document.querySelector(`#${rowId}`);
  if (value) {
    row.style.display = "flex";
    document.querySelector(`#${textId}`).textContent = value;
  } else {
    row.style.display = "none";
  }
}

// helper — switches which state panel is visible
function showState(state) {
  loadingPanel.style.display = "none";
  errorPanel.style.display = "none";
  notFoundPanel.style.display = "none";
  profilePanel.style.display = "none";

  if (state === "loading") loadingPanel.style.display = "block";
  if (state === "error") errorPanel.style.display = "block";
  if (state === "not-found") notFoundPanel.style.display = "block";
  if (state === "profile") profilePanel.style.display = "block";
}

// helper — formats large numbers with K suffix
function formatNumber(num) {
  if (num >= 1000) return (num / 1000).toFixed(1) + "k";
  return num;
}

Code Execution

Trace through searching "torvalds":

StepCodeResult
Validate"torvalds" not emptypass
Show loadingshowState("loading")spinner visible
Fetchfetch("https://api.github.com/users/torvalds")request sent
Responseresponse.status200
response.oktruecontinue
Parseresponse.json()user object
RenderrenderProfile(user)profile card filled
Show profileshowState("profile")card visible

Trace through searching "thisisnotarealuser999":

StepCodeResult
Fetchfetch("...thisisnotarealuser999")request sent
Responseresponse.status404
Checkresponse.status === 404true
ShowshowState("not-found")not found message

Trace through no internet connection:

StepCodeResult
Fetchfetch(...)network error thrown
Catcherr.message"Failed to fetch"
Checkerr.message.includes("fetch")true
ShowshowState("error")error panel visible
Message"Could not connect. Check your internet connection."shown

Output

Search "torvalds"               → Profile card with Linus Torvalds' data
Search "thisisnotarealuser999"  → "No GitHub user found with username..."
Search ""                       → "Please enter a username."
No internet connection          → "Could not connect. Check your internet..."

On this page