Build a Quiz App with Flask
Build a full-stack quiz app with a Python backend and JS frontend.
Introduction
30 Second Summary
Every time you take a quiz online, two things are happening at once. One part of the app stores the questions and checks your answers, while a completely separate part handles what you see on screen.
In this project, you will build a multiple-choice quiz app with two distinct layers: a Python backend that manages the data and logic, and a browser-based frontend that handles the user experience.
What You'll Build
A quiz web app where a Flask backend serves questions through a JSON API, and an HTML/CSS/JavaScript frontend displays them, collects answers, and shows your score.
By the end of this project, you'll have:
- A Flask backend with API endpoints that serve quiz questions and validate answers.
- An HTML/CSS frontend with a polished, dark-themed quiz interface.
- JavaScript logic that fetches data from the API and updates the page dynamically without reloading.
- A score tracker and results screen with replay functionality.
- Secret Mission: a countdown timer that forces the player to answer each question within 15 seconds.
Are there any prerequisites?
You should be comfortable with basic Python concepts like variables, functions, lists, and dictionaries. No web development experience is needed.
Set Up Your Project
Every web app starts with a clean workspace. In this project, your quiz app will have two layers: a Python backend that stores questions and checks answers, and an HTML/JavaScript frontend that displays the quiz to users.
Before writing any code, you need a dedicated project folder, an isolated Python environment, and the correct file structure. This step gets all of that ready.
In this step, get ready to:
- Create a project folder and virtual environment.
- Install Flask in your virtual environment.
- Create the file structure for your app.
Create your project folder and virtual environment
You need a terminal to run commands. The terminal is a text-based interface where you type instructions for your computer.
- Open the integrated terminal in VS Code by pressing Ctrl+` (the backtick key, usually above Tab).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir quiz-app
cd quiz-app
Now you need a virtual environment. This is an isolated space where you can install Python packages without affecting anything else on your computer.
- Create the virtual environment by running this command:
macOS
python3 -m venv .venv
Windows
py -3 -m venv .venv
What is a virtual environment?
The more Python projects you have, the more likely it is that you need different versions of Python libraries for each. A virtual environment is an isolated group of packages just for this project.
Packages you install here stay inside .venv and won't clash with anything else on your machine.
- Activate the virtual environment by running this command:
macOS
source .venv/bin/activate
Windows
.venv\Scripts\activate
Your terminal prompt now shows (.venv) at the beginning. This confirms you are working inside the virtual environment.
- Install Flask by running this command:
pip install Flask
What is Flask?
Flask is a lightweight Python web framework. It handles the plumbing of receiving web requests and sending back responses so you can focus on your app's logic.
You will use Flask to create the backend of your quiz app. It will serve your HTML page and provide a JSON API for question data.
- Verify Flask installed correctly by running this command:
python -c "import flask; print(flask.__version__)"
You should see a version number printed (for example, 3.1.3). This confirms Flask is available inside your virtual environment.
Don't see a version number?
- Make sure your terminal prompt shows (.venv) at the beginning. If it does not, re-run the activate command above.
- If you see ModuleNotFoundError, run pip install Flask again and make sure there are no errors in the output.
Help me figure out why Flask isn't installing in my virtual environment.
Create the project file structure
Your quiz app needs four files organized into specific folders. Flask expects templates (HTML) in a templates/ folder and static assets (CSS, JavaScript) in a static/ folder.
- Create the required folders by running these commands:
mkdir templates
mkdir static
- Create the empty project files by running these commands:
macOS
touch app.py
touch templates/index.html
touch static/style.css
touch static/script.js
Windows
New-Item -ItemType File -Path app.py
New-Item -ItemType File -Path templates\index.html
New-Item -ItemType File -Path static\style.css
New-Item -ItemType File -Path static\script.js
What does this file structure do?
- app.py is your backend. All of your Python/Flask code lives here.
- templates/index.html is the HTML page that Flask will serve to the browser.
- static/style.css controls how the quiz looks (colors, layout, spacing).
- static/script.js handles user interaction and communicates with the backend.
- Confirm all files and folders exist by running:
macOS
ls
ls templates
ls static
Windows
dir
dir templates
dir static
You should see app.py, templates/, and static/ in the first listing. Inside templates/ you should see index.html. Inside static/ you should see style.css and script.js.
Missing files or folders?
- Make sure you are inside the quiz-app folder when running these commands. Run pwd (macOS) or cd (Windows) to see your current location.
- If templates/ or static/ is missing, run the mkdir commands again before creating the files.
Help me fix my project file structure so it matches what's expected.
Your project skeleton is ready. Next up, you will write the Python backend that stores quiz questions and checks answers.
Build the Backend
Your project structure is in place. You have an empty app.py waiting to become the engine that powers your quiz.
In this step, you'll write the Python backend that stores quiz questions, serves them as JSON data, and checks whether a player's answer is correct.
In this step, get ready to:
- Write the Flask app with quiz data and three API routes.
- Test the backend by visiting an API endpoint in your browser.
Write the Flask application
The backend has two responsibilities: storing quiz data and exposing it through HTTP routes. You'll build this in three chunks inside app.py.
- Add the imports, create the Flask app instance, and define your quiz questions by copying this code into app.py:
from flask import Flask, render_template, jsonify, request
app = Flask(__name__)
questions = [
{
"id": 1,
"question": "What does HTML stand for?",
"options": [
"Hyper Text Markup Language",
"Hot Mail",
"How To Make Lasagna",
"Hyperlinks and Text Markup Language"
],
"answer": "Hyper Text Markup Language"
},
{
"id": 2,
"question": "Which planet is known as the Red Planet?",
"options": ["Venus", "Mars", "Jupiter", "Saturn"],
"answer": "Mars"
},
{
"id": 3,
"question": "What is the largest ocean on Earth?",
"options": [
"Atlantic Ocean",
"Indian Ocean",
"Arctic Ocean",
"Pacific Ocean"
],
"answer": "Pacific Ocean"
},
{
"id": 4,
"question": "In Python, which keyword is used to define a function?",
"options": ["func", "define", "def", "function"],
"answer": "def"
},
{
"id": 5,
"question": "What year did the World Wide Web become publicly available?",
"options": ["1989", "1991", "1995", "2000"],
"answer": "1991"
}
]
What does this code do?
from flask import Flask, render_template, jsonify, request imports the four Flask tools you need: the app factory, the template renderer, the JSON helper, and the request object for reading incoming data.
app = Flask(__name__) creates your Flask application instance. Every Flask project starts with this line.
questions is a Python list of dictionaries. Each dictionary holds an id, question text, a list of options, and the correct answer. This is your quiz data.
- Save app.py.
- You won't see a change yet. The next chunk adds routes that make this data accessible.
- Below the questions list, add the first two routes that handle GET requests:
@app.route("/")
def home():
return render_template("index.html")
@app.route("/api/questions")
def get_questions():
questions_without_answers = []
for q in questions:
questions_without_answers.append({
"id": q["id"],
"question": q["question"],
"options": q["options"]
})
return jsonify(questions_without_answers)
What do these routes do?
The / route serves your HTML page to the browser using render_template("index.html"). This is the page the player will see.
The /api/questions route is your first REST API endpoint. It loops through the questions and builds a new list that excludes the answer field. This prevents the frontend from seeing the correct answers before the player submits.
- Save app.py.
- You won't see a change yet. The final route and the server startup block come next.
- Below the get_questions function, add the answer-checking route and the startup block:
@app.route("/api/check", methods=["POST"])
def check_answer():
data = request.get_json()
question_id = data.get("question_id")
selected_answer = data.get("answer")
for q in questions:
if q["id"] == question_id:
correct = selected_answer == q["answer"]
return jsonify({
"correct": correct,
"correct_answer": q["answer"]
})
return jsonify({"error": "Question not found"}), 404
if __name__ == "__main__":
app.run(debug=True)
What does this route do?
request.get_json() reads the JSON body sent by the frontend. It expects a question_id and an answer value.
The function loops through the questions list to find a matching ID. If it matches, it compares the player's answer to the stored correct answer and returns a JSON response with correct (true or false) and correct_answer.
The if __name__ == "__main__" block lets you start the server by running python app.py directly.
- Save app.py.
Seeing red squiggles in your editor?
Make sure your virtual environment is activated. VS Code sometimes defaults to the system Python, which may not have Flask installed.
Check that every function is indented with 4 spaces under its def line. Python is strict about indentation.
Help me fix import or indentation errors in my Flask app.
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py before moving on.
ⓧ I'd like to double check the full code
from flask import Flask, render_template, jsonify, request
app = Flask(__name__)
questions = [
{
"id": 1,
"question": "What does HTML stand for?",
"options": [
"Hyper Text Markup Language",
"Hot Mail",
"How To Make Lasagna",
"Hyperlinks and Text Markup Language"
],
"answer": "Hyper Text Markup Language"
},
{
"id": 2,
"question": "Which planet is known as the Red Planet?",
"options": ["Venus", "Mars", "Jupiter", "Saturn"],
"answer": "Mars"
},
{
"id": 3,
"question": "What is the largest ocean on Earth?",
"options": [
"Atlantic Ocean",
"Indian Ocean",
"Arctic Ocean",
"Pacific Ocean"
],
"answer": "Pacific Ocean"
},
{
"id": 4,
"question": "In Python, which keyword is used to define a function?",
"options": ["func", "define", "def", "function"],
"answer": "def"
},
{
"id": 5,
"question": "What year did the World Wide Web become publicly available?",
"options": ["1989", "1991", "1995", "2000"],
"answer": "1991"
}
]
@app.route("/")
def home():
return render_template("index.html")
@app.route("/api/questions")
def get_questions():
questions_without_answers = []
for q in questions:
questions_without_answers.append({
"id": q["id"],
"question": q["question"],
"options": q["options"]
})
return jsonify(questions_without_answers)
@app.route("/api/check", methods=["POST"])
def check_answer():
data = request.get_json()
question_id = data.get("question_id")
selected_answer = data.get("answer")
for q in questions:
if q["id"] == question_id:
correct = selected_answer == q["answer"]
return jsonify({
"correct": correct,
"correct_answer": q["answer"]
})
return jsonify({"error": "Question not found"}), 404
if __name__ == "__main__":
app.run(debug=True)
Test the backend
Your Flask app is written. Time to start the development server and confirm the /api/questions endpoint returns real data.
- Start the Flask development server by running this command in your terminal:
flask --app app run --debug
What does this command do?
flask --app app tells Flask which file contains your application (app.py).
run --debug starts the server with auto-reload enabled. Whenever you save a file, Flask restarts automatically so you always see the latest version.
- You should see output that says Debug mode: on and shows the address http://127.0.0.1:5000.
- Open your browser and visit http://127.0.0.1:5000/api/questions.
- You should see a JSON array of question objects. Each object has id, question, and options fields, but no answer field. That means your backend is protecting the answers from the frontend.
Why no answer field?
The get_questions function deliberately strips out the answer before sending data to the browser. If the answers were included, a player could peek at the JSON and cheat.
The answers only travel from the server when the player submits a guess to /api/check. This is a core pattern in client-server architecture: the server keeps sensitive data private and only reveals what's needed.
Page not loading or seeing an error?
If you see ModuleNotFoundError: No module named 'flask', your virtual environment is not active. Run .venv\Scripts\activate and try again.
If you see a SyntaxError, double check your code against the full code tab above. Missing commas or mismatched brackets are the most common culprit.
Help me debug my Flask server startup error.
Keep the server running
Leave this terminal running for the rest of the project. The --debug flag means Flask auto-reloads whenever you save a file, so you never need to restart it manually.
You'll test the /api/check endpoint fully once the frontend is connected and can send POST requests.
Your backend is live and serving data. Next up, you'll build the HTML and CSS frontend that gives your quiz a polished interface.
Build the Frontend
Your backend is serving quiz data as JSON. Now you need a way for users to actually see and interact with those questions in their browser.
In this step, you'll build the frontend layer of your app. This includes the HTML structure (what the browser renders) and CSS (how it looks).
In this step, get ready to:
- Write the HTML structure for the quiz interface.
- Style the quiz with a dark-themed CSS design.
- Verify the page loads correctly in the browser.
Write the quiz HTML structure
The HTML file defines the structure of your quiz interface. It sets up all the containers and elements that CSS will style and JavaScript will control later.
- Open templates/index.html in VS Code and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quiz App</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<div id="quiz-screen">
<div class="header">
<h1>Quiz App</h1>
<p class="score">Score: <span id="score">0</span> / <span id="total">0</span></p>
</div>
<div id="question-container">
<h2 id="question-text">Loading...</h2>
<div id="options-container"></div>
</div>
<p id="feedback" class="feedback"></p>
<button id="next-btn" class="next-btn" style="display: none;">Next Question</button>
</div>
<div id="results-screen" style="display: none;">
<h1>Quiz Complete!</h1>
<p class="final-score">You scored <span id="final-score">0</span> out of <span id="final-total">0</span></p>
<p id="result-message"></p>
<button id="restart-btn" class="next-btn">Play Again</button>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>
What does this code do?
This HTML creates two screens that swap visibility as the quiz progresses.
- quiz-screen holds the active quiz: a header with the title and score, the question text, option buttons, feedback text, and a "Next Question" button.
- results-screen is hidden at first. It shows the final score and a "Play Again" button when the quiz ends.
- url_for('static', filename='...') is Flask's template syntax for linking to files in your static/ folder. Flask generates the correct URL path at render time.
- Save templates/index.html.
Seeing a TemplateSyntaxError?
- Make sure the double curly braces are intact around each url_for call. Flask needs both {{ and }} to recognize template expressions.
- Check that the file is saved inside the templates/ folder, not in the project root.
Help me fix a TemplateSyntaxError in my Flask index.html file.
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quiz App</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<div id="quiz-screen">
<div class="header">
<h1>Quiz App</h1>
<p class="score">Score: <span id="score">0</span> / <span id="total">0</span></p>
</div>
<div id="question-container">
<h2 id="question-text">Loading...</h2>
<div id="options-container"></div>
</div>
<p id="feedback" class="feedback"></p>
<button id="next-btn" class="next-btn" style="display: none;">Next Question</button>
</div>
<div id="results-screen" style="display: none;">
<h1>Quiz Complete!</h1>
<p class="final-score">You scored <span id="final-score">0</span> out of <span id="final-total">0</span></p>
<p id="result-message"></p>
<button id="restart-btn" class="next-btn">Play Again</button>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>
Style the quiz with CSS
The HTML skeleton is in place, but without CSS it will look like plain text on a white background. The stylesheet gives your quiz a polished, dark-themed card design.
- Open static/style.css and paste the following layout styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #1a1a2e;
color: #eee;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background-color: #16213e;
border-radius: 16px;
padding: 40px;
max-width: 600px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 1.5rem;
color: #e94560;
}
.score {
font-size: 1rem;
color: #aaa;
}
#question-text {
font-size: 1.3rem;
margin-bottom: 24px;
line-height: 1.4;
}
#options-container {
display: flex;
flex-direction: column;
gap: 12px;
}
What does this code do?
- The * reset removes default browser margins and padding so your layout starts clean.
- body uses flexbox to center the quiz card vertically and horizontally on a dark navy background.
- .container creates the rounded card with a slightly lighter background and a subtle shadow.
- .header places the title and score on opposite sides using flexbox.
- Below the code you just pasted, add the interactive element styles:
.option-btn {
padding: 14px 20px;
border: 2px solid #0f3460;
border-radius: 10px;
background-color: #0f3460;
color: #eee;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
text-align: left;
}
.option-btn:hover {
border-color: #e94560;
background-color: #1a1a4e;
}
.option-btn.correct {
border-color: #4caf50;
background-color: #1b5e20;
}
.option-btn.incorrect {
border-color: #f44336;
background-color: #b71c1c;
}
.option-btn.disabled {
pointer-events: none;
opacity: 0.7;
}
.feedback {
margin-top: 20px;
font-size: 1.1rem;
font-weight: bold;
min-height: 28px;
}
.feedback.correct {
color: #4caf50;
}
.feedback.incorrect {
color: #f44336;
}
.next-btn {
margin-top: 24px;
padding: 12px 30px;
background-color: #e94560;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.next-btn:hover {
background-color: #c73e54;
}
.final-score {
font-size: 1.5rem;
margin: 20px 0;
}
#result-message {
font-size: 1.1rem;
color: #aaa;
margin-bottom: 20px;
}
What does this code do?
- .option-btn styles the answer buttons with a dark blue fill and rounded corners. The :hover state gives a pink border highlight when the cursor hovers.
- .correct and .incorrect are classes that JavaScript will add later to turn buttons green or red after an answer is submitted.
- .disabled uses pointer-events: none to prevent clicking after the user has already answered.
- .next-btn styles both the "Next Question" and "Play Again" buttons with the app's pink accent colour.
- Save static/style.css.
Styles not showing up?
- Make sure the file is saved as static/style.css (inside the static/ folder), not in templates/ or the project root.
- Hard-refresh the browser with Ctrl+Shift+R (Windows) or Cmd+Shift+R (macOS) to clear any cached version of the empty stylesheet.
Help me debug why my CSS file isn't loading in Flask.
✔️ Awesome, I've got everything!
Great. Double check you've saved the file before moving on.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #1a1a2e;
color: #eee;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background-color: #16213e;
border-radius: 16px;
padding: 40px;
max-width: 600px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 1.5rem;
color: #e94560;
}
.score {
font-size: 1rem;
color: #aaa;
}
#question-text {
font-size: 1.3rem;
margin-bottom: 24px;
line-height: 1.4;
}
#options-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.option-btn {
padding: 14px 20px;
border: 2px solid #0f3460;
border-radius: 10px;
background-color: #0f3460;
color: #eee;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
text-align: left;
}
.option-btn:hover {
border-color: #e94560;
background-color: #1a1a4e;
}
.option-btn.correct {
border-color: #4caf50;
background-color: #1b5e20;
}
.option-btn.incorrect {
border-color: #f44336;
background-color: #b71c1c;
}
.option-btn.disabled {
pointer-events: none;
opacity: 0.7;
}
.feedback {
margin-top: 20px;
font-size: 1.1rem;
font-weight: bold;
min-height: 28px;
}
.feedback.correct {
color: #4caf50;
}
.feedback.incorrect {
color: #f44336;
}
.next-btn {
margin-top: 24px;
padding: 12px 30px;
background-color: #e94560;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.next-btn:hover {
background-color: #c73e54;
}
.final-score {
font-size: 1.5rem;
margin: 20px 0;
}
#result-message {
font-size: 1.1rem;
color: #aaa;
margin-bottom: 20px;
}
Verify the frontend loads
- Switch to your browser and navigate to http://127.0.0.1:5000/.
- If you already had the page open, hard-refresh with Ctrl+Shift+R (Windows) or Cmd+Shift+R (macOS).
You should see a dark-themed card in the centre of the page. The header reads "Quiz App" on the left and "Score: 0 / 0" on the right. The question area shows "Loading..." as placeholder text.
Why does it say Loading?
The HTML hardcodes "Loading..." as the default question text. JavaScript will replace it with real quiz data once you write the fetch logic in the next step. Since static/script.js is still empty, nothing updates the page after it loads.
Your quiz has a polished look, but it can't do anything yet. Next, you'll write JavaScript to connect this interface to your Flask backend and make the quiz fully interactive.
Connect Frontend to Backend
You have a backend that serves quiz data and a frontend that displays a styled card. Right now, these two sides don't talk to each other.
In this step, you'll write the JavaScript that bridges the gap. Your code will fetch quiz questions from the Flask API, display them as clickable buttons, check answers against the backend, and show a results screen at the end.
In this step, get ready to:
- Fetch quiz questions from your Flask API and display them on the page.
- Send answer choices to the backend and show correct/incorrect feedback.
- Add a results screen and replay functionality.
Fetch questions from the API and display them
The fetch API lets JavaScript make HTTP requests to your Flask backend. You'll use it to grab quiz questions from /api/questions and render them as interactive buttons.
- Open static/script.js in VS Code (it should be empty).
- Add the following code to set up your quiz state, grab references to the HTML elements, and define the functions that load and display questions:
let questions = [];
let currentIndex = 0;
let score = 0;
const questionText = document.getElementById("question-text");
const optionsContainer = document.getElementById("options-container");
const scoreDisplay = document.getElementById("score");
const totalDisplay = document.getElementById("total");
const feedback = document.getElementById("feedback");
const nextBtn = document.getElementById("next-btn");
const quizScreen = document.getElementById("quiz-screen");
const resultsScreen = document.getElementById("results-screen");
const finalScore = document.getElementById("final-score");
const finalTotal = document.getElementById("final-total");
const resultMessage = document.getElementById("result-message");
const restartBtn = document.getElementById("restart-btn");
async function loadQuestions() {
const response = await fetch("/api/questions");
questions = await response.json();
totalDisplay.textContent = questions.length;
finalTotal.textContent = questions.length;
displayQuestion();
}
function displayQuestion() {
const question = questions[currentIndex];
questionText.textContent = question.question;
optionsContainer.innerHTML = "";
feedback.textContent = "";
feedback.className = "feedback";
nextBtn.style.display = "none";
question.options.forEach(function (option) {
const button = document.createElement("button");
button.textContent = option;
button.className = "option-btn";
button.addEventListener("click", function () {
selectAnswer(question.id, option);
});
optionsContainer.appendChild(button);
});
}
What does this code do?
The first three let variables track the quiz state: which questions are loaded, which one you're on, and the current score. The const lines grab references to the HTML elements you built in the previous step so JavaScript can update them.
The loadQuestions() function calls your Flask API at /api/questions, converts the JSON response into a JavaScript array, updates the total counters, and calls displayQuestion() to render the first question.
The displayQuestion() function reads the current question from the array, sets the question text, and creates a clickable button for each answer option. Each button calls selectAnswer() when clicked (you'll write that next).
- Now add this single line at the very end of the file to kick off the quiz when the page loads:
loadQuestions();
Why is this at the bottom?
Placing loadQuestions() at the bottom of the file ensures all your functions are defined before the call executes. You'll keep adding code above this line in the next substeps.
- Save script.js and refresh http://127.0.0.1:5000/ in your browser.
- You should see the first quiz question ("What does HTML stand for?") with four option buttons displayed below it.
Still seeing "Loading..." instead of a question?
Make sure your Flask server is still running in the terminal. If it stopped, restart it with flask --app app run --debug.
Check the browser console (press F12 then click Console) for red error messages. A typo in an element ID (like question_text instead of question-text) will cause the script to fail silently.
help me debug why my quiz questions aren't loading
Add answer-checking logic
When a user clicks an option, the frontend needs to ask the backend whether it's correct. This uses a POST request to send the chosen answer, then highlights buttons green or red based on the response.
- In script.js, add the following function between displayQuestion() and the loadQuestions(); call at the bottom:
async function selectAnswer(questionId, selectedOption) {
const buttons = document.querySelectorAll(".option-btn");
buttons.forEach(function (btn) {
btn.classList.add("disabled");
});
const response = await fetch("/api/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question_id: questionId, answer: selectedOption })
});
const result = await response.json();
buttons.forEach(function (btn) {
if (btn.textContent === result.correct_answer) {
btn.classList.add("correct");
} else if (btn.textContent === selectedOption && !result.correct) {
btn.classList.add("incorrect");
}
});
if (result.correct) {
score++;
scoreDisplay.textContent = score;
feedback.textContent = "Correct!";
feedback.className = "feedback correct";
} else {
feedback.textContent = "Incorrect! The answer was: " + result.correct_answer;
feedback.className = "feedback incorrect";
}
nextBtn.style.display = "block";
}
What does this code do?
First, it disables all option buttons so the user can't click twice. Then it sends a POST request to /api/check with the question ID and selected answer as JSON. The Content-Type: application/json header tells Flask the body is JSON data.
When the response comes back, the code loops through all buttons. It highlights the correct answer green and (if the user was wrong) their choice red. Finally, it updates the score counter and shows the "Next Question" button.
- Save script.js and refresh your browser.
- Click any option button. You should see the correct answer highlighted green, an incorrect choice highlighted red (if you picked wrong), a "Correct!" or "Incorrect!" feedback message, and the score updating.
Clicking an option does nothing?
Open the browser console (F12 then Console). If you see a 415 error, make sure the headers: { "Content-Type": "application/json" } line is in your fetch call. Flask requires this header to parse JSON request bodies.
If you see "selectAnswer is not defined", check that your function is placed above the loadQuestions(); call at the bottom of the file.
help me fix my answer-checking fetch request
Add navigation and results
The quiz needs to advance through all questions and show a final score. You'll wire up the "Next Question" button, build a results screen, and add a "Play Again" feature.
- In script.js, add the following code between the selectAnswer() function and the loadQuestions(); call at the bottom:
nextBtn.addEventListener("click", function () {
currentIndex++;
if (currentIndex < questions.length) {
displayQuestion();
} else {
showResults();
}
});
function showResults() {
quizScreen.style.display = "none";
resultsScreen.style.display = "block";
finalScore.textContent = score;
const percentage = (score / questions.length) * 100;
if (percentage === 100) {
resultMessage.textContent = "Perfect score! You're a genius!";
} else if (percentage >= 60) {
resultMessage.textContent = "Great job! You know your stuff.";
} else {
resultMessage.textContent = "Keep learning! You'll get there.";
}
}
restartBtn.addEventListener("click", function () {
currentIndex = 0;
score = 0;
scoreDisplay.textContent = 0;
quizScreen.style.display = "block";
resultsScreen.style.display = "none";
displayQuestion();
});
What does this code do?
The nextBtn listener advances to the next question. If all questions are answered, it calls showResults() instead.
The showResults() function hides the quiz screen, shows the results screen, and displays a message based on the percentage score.
The restartBtn listener resets all state variables back to zero, switches back to the quiz screen, and calls displayQuestion() to start from question one.
- Save script.js and refresh your browser.
- Play through the entire quiz by clicking options and pressing "Next Question" after each one. After the last question, you should see a "Quiz Complete!" results screen with your final score and a personalized message.
- Click Play Again. The quiz should reset to question one with a score of 0.
Results screen not showing up?
Make sure the nextBtn event listener is placed after the selectAnswer() function. If it comes before, showResults won't be defined when the listener tries to call it.
If clicking "Next Question" does nothing, check that nextBtn.style.display = "block" appears at the end of your selectAnswer() function. Without it, the button stays hidden.
help me fix my quiz navigation and results screen
✔️ Awesome, I've got everything!
Great. Double check you've saved script.js and that your full quiz flow works end-to-end.
ⓧ I'd like to double check the full code
Here's the complete static/script.js file. Make sure yours matches exactly:
let questions = [];
let currentIndex = 0;
let score = 0;
const questionText = document.getElementById("question-text");
const optionsContainer = document.getElementById("options-container");
const scoreDisplay = document.getElementById("score");
const totalDisplay = document.getElementById("total");
const feedback = document.getElementById("feedback");
const nextBtn = document.getElementById("next-btn");
const quizScreen = document.getElementById("quiz-screen");
const resultsScreen = document.getElementById("results-screen");
const finalScore = document.getElementById("final-score");
const finalTotal = document.getElementById("final-total");
const resultMessage = document.getElementById("result-message");
const restartBtn = document.getElementById("restart-btn");
async function loadQuestions() {
const response = await fetch("/api/questions");
questions = await response.json();
totalDisplay.textContent = questions.length;
finalTotal.textContent = questions.length;
displayQuestion();
}
function displayQuestion() {
const question = questions[currentIndex];
questionText.textContent = question.question;
optionsContainer.innerHTML = "";
feedback.textContent = "";
feedback.className = "feedback";
nextBtn.style.display = "none";
question.options.forEach(function (option) {
const button = document.createElement("button");
button.textContent = option;
button.className = "option-btn";
button.addEventListener("click", function () {
selectAnswer(question.id, option);
});
optionsContainer.appendChild(button);
});
}
async function selectAnswer(questionId, selectedOption) {
const buttons = document.querySelectorAll(".option-btn");
buttons.forEach(function (btn) {
btn.classList.add("disabled");
});
const response = await fetch("/api/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question_id: questionId, answer: selectedOption })
});
const result = await response.json();
buttons.forEach(function (btn) {
if (btn.textContent === result.correct_answer) {
btn.classList.add("correct");
} else if (btn.textContent === selectedOption && !result.correct) {
btn.classList.add("incorrect");
}
});
if (result.correct) {
score++;
scoreDisplay.textContent = score;
feedback.textContent = "Correct!";
feedback.className = "feedback correct";
} else {
feedback.textContent = "Incorrect! The answer was: " + result.correct_answer;
feedback.className = "feedback incorrect";
}
nextBtn.style.display = "block";
}
nextBtn.addEventListener("click", function () {
currentIndex++;
if (currentIndex < questions.length) {
displayQuestion();
} else {
showResults();
}
});
function showResults() {
quizScreen.style.display = "none";
resultsScreen.style.display = "block";
finalScore.textContent = score;
const percentage = (score / questions.length) * 100;
if (percentage === 100) {
resultMessage.textContent = "Perfect score! You're a genius!";
} else if (percentage >= 60) {
resultMessage.textContent = "Great job! You know your stuff.";
} else {
resultMessage.textContent = "Keep learning! You'll get there.";
}
}
restartBtn.addEventListener("click", function () {
currentIndex = 0;
score = 0;
scoreDisplay.textContent = 0;
quizScreen.style.display = "block";
resultsScreen.style.display = "none";
displayQuestion();
});
loadQuestions();
Your quiz app is fully functional. You've connected a JavaScript frontend to a Python backend using HTTP requests and JSON. Next up, you'll take on a bonus challenge to add time pressure to the game.
Secret mission
Add a Countdown Timer
Your quiz works great, but there is no pressure. What if each question had a countdown timer? Add a 15-second timer that ticks down, flashes a warning when time is running low, and automatically moves to the next question when time runs out.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The quiz-app project folder on your Desktop (includes all code files and the virtual environment).
- The Flask development server running in your terminal.
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with your quiz app.
- Your Flask server continues running at http://127.0.0.1:5000/.
- Your quiz-app folder stays on your Desktop with all files intact.
- You can return to VS Code and pick up where you left off any time.
Pause - I'll come back to this later
Shut down the running server to free up your terminal, but keep all your files so you can resume later.
- In your VS Code terminal, press Ctrl+C to stop the Flask development server.
- Type deactivate to exit the virtual environment.
- Your quiz-app folder remains on your Desktop. To resume later, open the folder in VS Code, activate the virtual environment with .venv\Scripts\activate, and run flask --app app run --debug.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes everything you built in this project.
- In your VS Code terminal, press Ctrl+C to stop the Flask development server.
- Type deactivate to exit the virtual environment.
- Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rmdir /s /q quiz-app
macOS or Linux users
If you are on macOS or Linux, use this command instead:
cd ~/Desktop
rm -rf quiz-app
Nice Work!
Nice Work!
You just built a fully functional quiz web app from scratch. You wrote the backend logic in Python, designed the frontend interface in HTML and CSS, and wired them together with JavaScript.
You've learned how to:
- Build a Flask backend with API endpoints that serve quiz data and validate answers using JSON.
- Create an HTML/CSS frontend with a polished dark-themed interface and interactive option buttons.
- Connect frontend to backend using JavaScript fetch requests, handling both GET and POST methods to build a complete client-server architecture.
- Secret Mission: Added a countdown timer with visual warnings and auto-advance on timeout using setInterval and clearInterval.
Ready to quiz yourself?