Back to Articles
TutorialsPublished August 22, 202616 min read

Web Foundations Lab: Connecting Frontend and Backend with APIs

"A beginner-friendly, hands-on lab that teaches how the frontend and backend communicate — by building a GitHub Profile Finder using the Fetch API, REST, and a Node.js + Express backend."
Raphael Johnathan F. Flores

Raphael Johnathan F. Flores

2x AWS Certified Cloud Architect

#JavaScript#REST API#Fetch API#Node.js#Express#Backend#Web Foundations#Beginner
Web Foundations Lab: Connecting Frontend and Backend with APIs

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:

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 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

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).

Explore All Articles