The Question Every Beginner Hits
You have built a nice-looking page with HTML and CSS. You have added interactivity with JavaScript. Then someone asks: where does the data come from?
That question is the bridge between frontend and backend development. This lab answers it concretely by building a GitHub Profile Finder — a web app where a user types any GitHub username and sees that person’s profile data fetched live from the internet.
By the end of this lab you will understand:
- What a backend is and what problem it solves.
- What an API and REST mean in plain terms.
- How the frontend sends requests and receives responses using
fetch. - How to build your own simple backend with Node.js and Express.
- How the two sides talk to each other.
This lab continues from where Web Foundations Lab 01 (HTML, CSS, JavaScript) left off. Basic JavaScript familiarity is assumed.
The Mental Model First
Before writing a single line of code, let’s establish the mental model. Think of it like ordering food at a restaurant.
- You (the customer) are the frontend — the browser, the visible interface.
- The kitchen is the backend — a server running code that processes requests and returns data.
- The waiter is the API — a defined contract that describes what you can order, how to ask for it, and what you will receive back.
You do not walk into the kitchen yourself. You place an order (a request) with the waiter, and the kitchen sends back a plate of food (a response). The frontend never directly touches the database or the business logic — it communicates exclusively through the API.
[ Browser / Frontend ]
|
| HTTP Request (GET /api/user/nthndkid)
v
[ API Server / Backend ]
|
| Queries database or external service
v
[ Database / External API ]
|
| Returns data
v
[ API Server / Backend ]
|
| HTTP Response (200 OK, JSON body)
v
[ Browser / Frontend ]
|
| Renders the data on screen
Part 1 — What Is REST?
REST (Representational State Transfer) is the most common architectural style for APIs on the web. A REST API is just a set of URLs (called endpoints) that your frontend can call to read or modify data.
Each HTTP request has a method that describes the intended action:
| Method | Action | Example |
|---|---|---|
GET |
Read data | GET /users/nthndkid — fetch a user profile |
POST |
Create data | POST /posts — create a new blog post |
PUT |
Replace data | PUT /posts/42 — fully update a post |
PATCH |
Partially update | PATCH /posts/42 — update just the title |
DELETE |
Delete data | DELETE /posts/42 — remove a post |
Each response comes back with a status code that tells you what happened:
| Code | Meaning |
|---|---|
200 OK |
Request succeeded |
201 Created |
Resource was created successfully |
400 Bad Request |
The request was malformed |
401 Unauthorized |
Authentication required |
403 Forbidden |
Authenticated but not allowed |
404 Not Found |
The resource does not exist |
500 Internal Server Error |
Something broke on the server |
Responses almost always return data as JSON — JavaScript Object Notation. It looks like a JavaScript object and maps directly to one:
{
"login": "nthndkid",
"name": "Raphael Flores",
"public_repos": 24,
"followers": 120,
"avatar_url": "https://avatars.githubusercontent.com/u/..."
}
Part 2 — Project Setup
Create a new folder called github-finder/ with this structure:
github-finder/
frontend/
index.html
styles.css
script.js
backend/
server.js
package.json
We will build the frontend first using GitHub’s free public API directly, then layer in our own backend to understand why a backend is necessary.
Part 3 — The Frontend: Calling a Public API
GitHub exposes a public REST API with no authentication required for basic profile lookups. The endpoint is:
GET https://api.github.com/users/{username}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GitHub Profile Finder</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="page">
<header class="app-header">
<h1 class="app-title">GitHub Profile Finder</h1>
<p class="app-subtitle">Search any GitHub username and see their profile.</p>
</header>
<!-- Search form -->
<form class="search-form" id="search-form">
<input
class="search-input"
id="search-input"
type="text"
placeholder="Enter a GitHub username..."
autocomplete="off"
required
/>
<button class="search-btn" type="submit">Search</button>
</form>
<!-- State displays -->
<p class="status-msg hidden" id="loading-msg">Loading...</p>
<p class="status-msg error hidden" id="error-msg"></p>
<!-- Result card -->
<article class="profile-card hidden" id="profile-card">
<img class="avatar" id="avatar" src="" alt="" width="80" height="80" />
<div class="profile-info">
<h2 class="profile-name" id="profile-name"></h2>
<p class="profile-username" id="profile-username"></p>
<p class="profile-bio" id="profile-bio"></p>
<ul class="profile-stats">
<li><span id="stat-repos"></span> Repos</li>
<li><span id="stat-followers"></span> Followers</li>
<li><span id="stat-following"></span> Following</li>
</ul>
<a class="profile-link" id="profile-link" target="_blank" rel="noopener noreferrer">
View on GitHub
</a>
</div>
</article>
</main>
<script src="script.js"></script>
</body>
</html>
styles.css
*,
*::before,
*::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f1f5f9;
--surface: #ffffff;
--text: #0f172a;
--muted: #64748b;
--accent: #2563eb;
--danger: #dc2626;
--radius: 12px;
--shadow: 0 4px 24px rgba(0,0,0,0.08);
--font-sans: 'Segoe UI', system-ui, sans-serif;
--font-mono: 'Consolas', monospace;
--transition: 180ms ease;
}
body {
font-family: var(--font-sans);
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.page {
max-width: 560px;
margin: 0 auto;
padding: 4rem 1.5rem;
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Header */
.app-title { font-size: 1.75rem; font-weight: 700; letter-spacing: -0.02em; }
.app-subtitle{ font-size: 0.9rem; color: var(--muted); margin-top: 0.35rem; }
/* Search form */
.search-form {
display: flex;
gap: 0.75rem;
}
.search-input {
flex: 1;
font-family: var(--font-sans);
font-size: 0.9rem;
padding: 0.65rem 1rem;
border: 1px solid #cbd5e1;
border-radius: var(--radius);
outline: none;
transition: border-color var(--transition), box-shadow var(--transition);
}
.search-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(37,99,235,0.15);
}
.search-btn {
font-family: var(--font-sans);
font-weight: 600;
font-size: 0.9rem;
padding: 0.65rem 1.5rem;
background: var(--accent);
color: #fff;
border: none;
border-radius: var(--radius);
cursor: pointer;
transition: background var(--transition);
}
.search-btn:hover { background: #1d4ed8; }
/* Status messages */
.status-msg { font-size: 0.9rem; color: var(--muted); text-align: center; }
.status-msg.error { color: var(--danger); }
/* Profile card */
.profile-card {
display: flex;
gap: 1.5rem;
background: var(--surface);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.75rem;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
flex-shrink: 0;
border: 2px solid #e2e8f0;
}
.profile-info { display: flex; flex-direction: column; gap: 0.4rem; }
.profile-name { font-size: 1.1rem; font-weight: 700; }
.profile-username { font-family: var(--font-mono); font-size: 0.8rem; color: var(--accent); }
.profile-bio { font-size: 0.875rem; color: var(--muted); line-height: 1.5; }
.profile-stats {
display: flex;
gap: 1.25rem;
list-style: none;
font-size: 0.8rem;
color: var(--muted);
margin-top: 0.25rem;
}
.profile-stats span {
font-family: var(--font-mono);
font-weight: 700;
color: var(--text);
}
.profile-link {
font-size: 0.8rem;
font-weight: 600;
color: var(--accent);
text-decoration: none;
margin-top: 0.25rem;
width: fit-content;
}
.profile-link:hover { text-decoration: underline; }
/* Utility */
.hidden { display: none; }
script.js
This is where the frontend-backend connection happens:
// --- Element references ---
const form = document.getElementById('search-form');
const input = document.getElementById('search-input');
const loadingMsg = document.getElementById('loading-msg');
const errorMsg = document.getElementById('error-msg');
const card = document.getElementById('profile-card');
// Profile card fields
const avatar = document.getElementById('avatar');
const name = document.getElementById('profile-name');
const username = document.getElementById('profile-username');
const bio = document.getElementById('profile-bio');
const repos = document.getElementById('stat-repos');
const followers = document.getElementById('stat-followers');
const following = document.getElementById('stat-following');
const link = document.getElementById('profile-link');
// --- UI state helpers ---
const show = (el) => el.classList.remove('hidden');
const hide = (el) => el.classList.add('hidden');
const resetUI = () => {
hide(loadingMsg);
hide(errorMsg);
hide(card);
errorMsg.textContent = '';
};
// --- Core fetch function ---
const fetchGitHubProfile = async (githubUsername) => {
// This is the API call — we are sending a GET request to GitHub's REST API
const response = await fetch(`https://api.github.com/users/${githubUsername}`);
// The API communicates success or failure through status codes
if (response.status === 404) {
throw new Error(`User "${githubUsername}" not found.`);
}
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
// Parse the JSON body of the response
const data = await response.json();
return data;
};
// --- Render the profile data into the DOM ---
const renderProfile = (data) => {
avatar.src = data.avatar_url;
avatar.alt = `Avatar for ${data.login}`;
name.textContent = data.name || data.login;
username.textContent= `@${data.login}`;
bio.textContent = data.bio || 'No bio provided.';
repos.textContent = data.public_repos;
followers.textContent = data.followers;
following.textContent = data.following;
link.href = data.html_url;
link.textContent = `github.com/${data.login}`;
};
// --- Form submission handler ---
form.addEventListener('submit', async (event) => {
event.preventDefault();
const query = input.value.trim();
if (!query) return;
resetUI();
show(loadingMsg);
try {
const data = await fetchGitHubProfile(query);
hide(loadingMsg);
renderProfile(data);
show(card);
} catch (error) {
hide(loadingMsg);
errorMsg.textContent = error.message;
show(errorMsg);
}
});
Open frontend/index.html in your browser and search for any GitHub username — including your own. You are making a live HTTP GET request to GitHub’s servers and rendering real data returned as JSON.
That is the frontend-backend connection in action. Your browser is the frontend. GitHub’s API server is the backend. The fetch call is the communication channel between them.
Part 4 — Why You Need Your Own Backend
Using a public API directly from the browser works for learning. But in production, it creates real problems:
Problem 1 — API Keys in Plain Sight Most real APIs require secret keys for authentication. If you call those APIs directly from the frontend, anyone who opens DevTools can read your key and abuse it.
Problem 2 — CORS Restrictions APIs often block requests that originate from browser-based JavaScript for security reasons (Cross-Origin Resource Sharing). A backend server can make those calls on behalf of the browser without hitting these restrictions.
Problem 3 — Business Logic Exposure Any data transformation, filtering, or business logic you write in the frontend is visible to anyone. Sensitive logic belongs on a server where users cannot inspect or manipulate it.
The solution: your frontend calls your backend, and your backend calls the third-party API. The secret key never leaves your server.
Browser --> Your Backend --> GitHub API
<-- <--
Part 5 — Building the Backend with Node.js and Express
Node.js lets you run JavaScript on a server. Express is a lightweight framework that makes it easy to define API routes.
Setup
Inside the backend/ folder, open a terminal and run:
npm init -y
npm install express node-fetch
server.js
import express from 'express';
import fetch from 'node-fetch';
const app = express();
const PORT = 3001;
// Allow the frontend (running on a different port) to call this server
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
next();
});
// Our single API route — the frontend calls this instead of GitHub directly
app.get('/api/github/:username', async (req, res) => {
const { username } = req.params;
try {
// The backend makes the call to GitHub (where you could safely use a secret token)
const githubResponse = await fetch(`https://api.github.com/users/${username}`, {
headers: {
// In a real app: Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
'User-Agent': 'github-finder-app'
}
});
if (githubResponse.status === 404) {
return res.status(404).json({ error: `User "${username}" not found.` });
}
if (!githubResponse.ok) {
return res.status(githubResponse.status).json({ error: 'GitHub API error.' });
}
const data = await githubResponse.json();
// Return only the fields the frontend actually needs — a clean, minimal response
res.json({
login: data.login,
name: data.name,
bio: data.bio,
avatar_url: data.avatar_url,
html_url: data.html_url,
public_repos: data.public_repos,
followers: data.followers,
following: data.following
});
} catch (error) {
res.status(500).json({ error: 'Internal server error.' });
}
});
app.listen(PORT, () => {
console.log(`Backend running at http://localhost:${PORT}`);
});
Add "type": "module" to package.json to enable ES module syntax:
{
"name": "github-finder-backend",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"node-fetch": "^3.3.2"
}
}
Start the server:
node server.js
# Backend running at http://localhost:3001
Update the Frontend to Call Your Backend
In script.js, change the URL in fetchGitHubProfile from the GitHub API to your own backend:
// Before — calling GitHub directly
const response = await fetch(`https://api.github.com/users/${githubUsername}`);
// After — calling your own backend, which calls GitHub on your behalf
const response = await fetch(`http://localhost:3001/api/github/${githubUsername}`);
The frontend code stays almost identical. The only change is the URL. The GitHub API is now invisible to the browser — only your backend knows about it.
What You Built and What It Demonstrates
| Concept | Where It Appears |
|---|---|
| HTTP GET request | fetch() call in script.js |
| JSON response parsing | response.json() and renderProfile() |
| Status code handling | response.status === 404 check |
| Error handling | try/catch block with user-facing error message |
| REST API endpoint design | GET /api/github/:username in server.js |
| Route parameters | req.params.username in Express |
| CORS headers | Access-Control-Allow-Origin middleware |
| API proxying | Backend calling GitHub on behalf of the frontend |
| Separation of concerns | Frontend renders data; backend fetches and filters it |
The Request-Response Cycle — Full Picture
1. User types "nthndkid" and clicks Search
2. Browser sends: GET http://localhost:3001/api/github/nthndkid
3. Express server receives the request, extracts "nthndkid" from the URL
4. Server sends: GET https://api.github.com/users/nthndkid
5. GitHub responds: 200 OK { "login": "nthndkid", "name": "Raphael Flores", ... }
6. Server filters the data and responds: 200 OK { filtered JSON }
7. Browser receives the response, script.js calls renderProfile(data)
8. DOM updates — the profile card appears on screen
Every web application you have ever used — from Instagram to GitHub itself — operates on this same cycle. The sophistication scales, but the pattern does not change.
Key Takeaways
- The frontend handles what the user sees and interacts with. The backend handles data, business logic, and secrets.
- APIs are contracts — defined endpoints that describe how the frontend and backend communicate.
- HTTP methods (
GET,POST,PUT,DELETE) describe the action. Status codes describe the outcome. fetchis the browser’s built-in tool for making HTTP requests.- Always
awaitboth thefetchand the.json()call, and always wrap them intry/catch. - Your own backend acts as a secure proxy — the frontend never exposes API keys or internal logic.
From here, the natural next steps are: deploying your backend to AWS (Lambda + API Gateway for serverless, or EC2 for a traditional server), connecting a real database (DynamoDB, PostgreSQL), and adding authentication (AWS Cognito, JWT).
