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 shownLogic
- Select the input, button, and display area
- On search — validate that the input is not empty
- Show loading state — hide previous results
- Fetch from
https://api.github.com/users/{username} - If response is 404 — show not found message
- If response is not ok — throw a general error
- Parse JSON and render the profile card
- Fetch their repositories separately for a repo count
- Catch any network errors and show an error state
Flow
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":
| Step | Code | Result |
|---|---|---|
| Validate | "torvalds" not empty | pass |
| Show loading | showState("loading") | spinner visible |
| Fetch | fetch("https://api.github.com/users/torvalds") | request sent |
| Response | response.status | 200 |
response.ok | true | continue |
| Parse | response.json() | user object |
| Render | renderProfile(user) | profile card filled |
| Show profile | showState("profile") | card visible |
Trace through searching "thisisnotarealuser999":
| Step | Code | Result |
|---|---|---|
| Fetch | fetch("...thisisnotarealuser999") | request sent |
| Response | response.status | 404 |
| Check | response.status === 404 | true |
| Show | showState("not-found") | not found message |
Trace through no internet connection:
| Step | Code | Result |
|---|---|---|
| Fetch | fetch(...) | network error thrown |
| Catch | err.message | "Failed to fetch" |
| Check | err.message.includes("fetch") | true |
| Show | showState("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..."