Build 3 real AI-powered web applications using the Claude API, pure HTML/CSS/JS, and GitHub Pages. No frameworks. No servers. Just ship.
In this course you will build 3 real AI-powered web applications and deploy them live on GitHub Pages using the Anthropic Claude API:
We use no frameworks, no Node.js, no build tools. Everything runs in the browser and deploys to GitHub Pages for free. This is intentional — the simpler the stack, the faster you focus on the AI logic itself.
GitHub Pages is a free hosting service for static HTML/CSS/JS websites. Create a repository, push your files, enable Pages in settings, and your site is live at yourusername.github.io/repo-name. No servers, no costs, no configuration.
The Anthropic API uses standard HTTP requests. From a browser, you use the fetch() function with your API key in the headers and your prompt in the request body.
async function askClaude(question) {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "YOUR_API_KEY",
"anthropic-version": "2023-06-01"
},
body: JSON.stringify({
model: "claude-opus-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: question }]
})
});
const data = await response.json();
return data.content[0].text;
}
{
"content": [{ "type": "text", "text": "Claude's reply here" }],
"model": "claude-opus-4-5",
"usage": { "input_tokens": 15, "output_tokens": 127 }
}
try {
const result = await askClaude("Hello!");
document.getElementById("output").textContent = result;
} catch (error) {
console.error("API Error:", error);
document.getElementById("output").textContent = "Error: " + error.message;
}
Calling the Anthropic API directly from the browser exposes your API key in the JavaScript source code. For a production app, you should route requests through a server or Cloudflare Worker. For learning and personal projects, direct browser calls are acceptable.
<div class="chat-container">
<div class="chat-messages" id="messages"></div>
<div class="chat-input-row">
<textarea id="user-input" placeholder="Type a message..."></textarea>
<button onclick="sendMessage()">Send</button>
</div>
</div>
const messages = [];
async function sendMessage() {
const input = document.getElementById("user-input");
const text = input.value.trim();
if (!text) return;
// Add user message to UI
addMessageToUI("user", text);
// Add to conversation history
messages.push({ role: "user", content: text });
input.value = "";
// Get AI response
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json",
"x-api-key": API_KEY, "anthropic-version": "2023-06-01" },
body: JSON.stringify({ model: "claude-opus-4-5", max_tokens: 1024,
messages: messages })
});
const data = await response.json();
const reply = data.content[0].text;
// Add AI response to UI and history
addMessageToUI("assistant", reply);
messages.push({ role: "assistant", content: reply });
}
function addMessageToUI(role, text) {
const msg = document.createElement("div");
msg.className = "message " + role;
msg.textContent = text;
document.getElementById("messages").appendChild(msg);
}
A system prompt is a set of instructions you send before the conversation starts that defines how your AI should behave. Users do not see it — it runs silently in the background, shaping every response.
const response = await fetch("https://api.anthropic.com/v1/messages", {
// ... headers ...
body: JSON.stringify({
model: "claude-opus-4-5",
max_tokens: 1024,
system: `You are Sola, a friendly learning assistant for SWAL Learn.
Your rules:
- Only help with topics related to technology, programming, and learning
- Always encourage students when they struggle
- Use simple language appropriate for beginners
- When answering code questions, always include a working example
- If asked about unrelated topics, gently redirect to tech learning`,
messages: messages
})
});
By default, the API waits until Claude has finished generating the complete response before sending it to you. With streaming, text is sent as it is generated — token by token — creating a typewriter effect that makes responses feel instant rather than making users wait.
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY,
"anthropic-version": "2023-06-01"
},
body: JSON.stringify({
model: "claude-opus-4-5",
max_tokens: 1024,
stream: true, // Enable streaming
messages: messages
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullText = "";
const messageDiv = document.createElement("div");
messageDiv.className = "message assistant";
document.getElementById("messages").appendChild(messageDiv);
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const json = line.slice(6);
if (json === "[DONE]") continue;
try {
const parsed = JSON.parse(json);
if (parsed.type === "content_block_delta") {
fullText += parsed.delta.text;
messageDiv.textContent = fullText;
}
} catch (e) {}
}
}
}
In this lesson you build a tool that analyses any text and returns three insights: overall sentiment, a brief summary, and the key phrases. All from a single API call with a carefully designed prompt.
const analyseText = async (text) => {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY,
"anthropic-version": "2023-06-01"
},
body: JSON.stringify({
model: "claude-opus-4-5",
max_tokens: 512,
messages: [{
role: "user",
content: `Analyse the following text. Return ONLY a valid JSON object
with no additional text, markdown, or code fences:
{
"sentiment": "positive" or "negative" or "neutral" or "mixed",
"sentiment_score": a number from -1.0 (very negative) to 1.0 (very positive),
"summary": "One sentence summary of the main point",
"key_phrases": ["phrase1", "phrase2", "phrase3"],
"tone": "formal" or "casual" or "emotional" or "technical"
}
Text to analyse: "${text}"`
}]
})
});
const data = await response.json();
return JSON.parse(data.content[0].text);
};
const result = await analyseText(userText);
sentimentEl.textContent = result.sentiment;
scoreEl.textContent = result.sentiment_score.toFixed(2);
summaryEl.textContent = result.summary;
result.key_phrases.forEach(phrase => {
const tag = document.createElement("span");
tag.className = "phrase-tag";
tag.textContent = phrase;
phrasesEl.appendChild(tag);
});
The Study Buddy generates a quiz on any topic in seconds. The user enters a topic and number of questions, the AI generates a structured quiz, and the app handles scoring and feedback.
const generateQuiz = async (topic, numQuestions) => {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json",
"x-api-key": API_KEY, "anthropic-version": "2023-06-01" },
body: JSON.stringify({
model: "claude-opus-4-5",
max_tokens: 2048,
messages: [{
role: "user",
content: `Generate a quiz about "${topic}" with ${numQuestions} questions.
Return ONLY a valid JSON array with no additional text:
[{
"question": "The question text",
"options": ["Option A", "Option B", "Option C", "Option D"],
"correct": 0,
"explanation": "Why this answer is correct"
}]
Each question must have exactly 4 options. correct is the 0-based index.`
}]
})
});
const data = await response.json();
return JSON.parse(data.content[0].text);
};
function renderQuiz(questions) {
questions.forEach((q, index) => {
const qDiv = document.createElement("div");
qDiv.className = "question";
qDiv.innerHTML = `<p>${index + 1}. ${q.question}</p>`;
q.options.forEach((opt, i) => {
const btn = document.createElement("button");
btn.textContent = opt;
btn.onclick = () => checkAnswer(btn, i, q.correct, q.explanation, qDiv);
qDiv.appendChild(btn);
});
quizContainer.appendChild(qDiv);
});
}
Production AI apps encounter many types of errors. Handling them gracefully is what separates a professional app from a broken prototype.
async function callAPI(messages, retries = 3) {
for (let attempt = 0; attempt < retries; attempt++) {
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
// ... config ...
});
if (!response.ok) {
const error = await response.json();
if (response.status === 429 || response.status === 529) {
// Wait before retrying: 1s, 2s, 4s (exponential backoff)
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
continue;
}
throw new Error(`API Error ${response.status}: ${error.error?.message}`);
}
return await response.json();
} catch (error) {
if (attempt === retries - 1) throw error;
}
}
}
The difference between a toy project and a professional AI app is often not the AI — it is the user experience around the AI. Loading states, typing indicators, copy buttons, and clear error messages make an app feel polished and trustworthy.
async function sendMessage() {
const btn = document.getElementById("send-btn");
const input = document.getElementById("user-input");
// Disable UI during request
btn.disabled = true;
btn.textContent = "Thinking...";
input.disabled = true;
// Show typing indicator
const typing = createTypingIndicator();
messagesEl.appendChild(typing);
try {
const response = await getAIResponse();
typing.remove();
displayMessage("assistant", response);
} catch (error) {
typing.remove();
displayError(error.message);
} finally {
// Always re-enable UI
btn.disabled = false;
btn.textContent = "Send";
input.disabled = false;
input.focus();
}
}
function addCopyButton(messageDiv, text) {
const copyBtn = document.createElement("button");
copyBtn.className = "copy-btn";
copyBtn.textContent = "Copy";
copyBtn.onclick = async () => {
await navigator.clipboard.writeText(text);
copyBtn.textContent = "Copied!";
setTimeout(() => copyBtn.textContent = "Copy", 2000);
};
messageDiv.appendChild(copyBtn);
}
GitHub Pages deploys your HTML/CSS/JS app for free with zero configuration. Anyone in the world can access it at a public URL. This lesson walks through the complete deployment process.
my-ai-app/ ├── index.html (main chat interface) ├── analyser.html (text analyser) ├── study-buddy.html (quiz generator) ├── style.css (shared styles) ├── app.js (shared JavaScript) └── README.md (project documentation)
https://yourusername.github.io/repo-name⚠️ Your API key will be visible in the browser developer tools of anyone who visits your site. For a course project this is acceptable. For a production app serving real users, you must hide the key behind a server (Cloudflare Worker, backend API) so it is never exposed in browser JavaScript.