1. The Problem
Build a multiple-choice quiz that presents questions one at a time (in the terminal or browser), accepts answers, and shows the final score with correct/incorrect feedback.
Why this project? Separating data (the question bank) from logic (the quiz runner) from presentation (the UI) — the three-layer pattern used in every real application.
2. How to Think About This
Store questions as an array of objects — each with a question string, an options array, and the index of the correct answer. The quiz runner is a pure function that takes the questions and user answers and returns results. The CLI or browser UI handles the back-and-forth with the user. Three separate layers, each testable independently.
3. The Build
import * as readline from "readline/promises";
import { stdin, stdout } from "process";
// ── Question bank ──────────────────────────────────────────────────────────
const questions = [
{
q: "What keyword declares a block-scoped variable in modern JS?",
options: ["var", "let", "dim", "val"],
answer: 1,
},
{
q: "Which value is NOT falsy in JavaScript?",
options: ["0", """", "[]", "null"],
answer: 2, // [] is truthy!
},
{
q: "What does typeof null return?",
options: [""null"", ""undefined"", ""object"", ""boolean""],
answer: 2,
},
{
q: "Which method adds an item to the END of an array?",
options: ["unshift", "shift", "push", "pop"],
answer: 2,
},
{
q: "What does === check?",
options: ["Value only", "Type only", "Value AND type", "Reference"],
answer: 2,
},
];
// ── Pure quiz engine ───────────────────────────────────────────────────────
export function gradeAnswers(answers, qs = questions) {
let score = 0;
const results = qs.map((q, i) => {
const correct = answers[i] === q.answer;
if (correct) score++;
return { question: q.q, correct,
yourAnswer: q.options[answers[i]] ?? "not answered",
correctAnswer: q.options[q.answer] };
});
return { score, total: qs.length, percent: Math.round(score / qs.length * 100), results };
}
// ── CLI presentation ───────────────────────────────────────────────────────
const rl = readline.createInterface({ input: stdin, output: stdout });
const answers = [];
console.log("\n=== JavaScript Quiz ===");
console.log(`${questions.length} questions. Enter the number of your answer.\n`);
for (let i = 0; i < questions.length; i++) {
const q = questions[i];
console.log(`Q${i + 1}: ${q.q}`);
q.options.forEach((opt, idx) => console.log(` ${idx + 1}. ${opt}`));
let answer = null;
while (answer === null) {
const raw = await rl.question("Your answer (1-4): ");
const n = parseInt(raw, 10) - 1; // convert 1-based to 0-based index
if (n >= 0 && n < q.options.length) {
answer = n;
} else {
console.log("Please enter a number between 1 and", q.options.length);
}
}
answers.push(answer);
console.log();
}
rl.close();
// Display results
const { score, total, percent, results } = gradeAnswers(answers);
console.log("=== Results ===");
results.forEach((r, i) => {
const icon = r.correct ? "✓" : "✗";
console.log(`\n${icon} Q${i + 1}: ${r.question}`);
if (!r.correct) {
console.log(` Your answer: ${r.yourAnswer}`);
console.log(` Correct: ${r.correctAnswer}`);
}
});
console.log(`\nScore: ${score}/${total} (${percent}%)`);
if (percent === 100) console.log("Perfect score! 🎉");
else if (percent >= 80) console.log("Great work! 🌟");
else if (percent >= 60) console.log("Good effort — review the questions you missed.");
else console.log("Keep studying — you've got this!");
4. Test & Prove
import { gradeAnswers } from "./quiz.mjs";
const sampleQs = [
{ q:"Q1", options:["a","b","c"], answer:0 },
{ q:"Q2", options:["x","y","z"], answer:2 },
{ q:"Q3", options:["p","q"], answer:1 },
];
let pass=0, fail=0;
function test(desc,fn){try{fn();console.log(" ✓",desc);pass++;}catch(e){console.log(" ✗",desc,"—",e.message);fail++;}}
test("all correct = 100%", () => {
const r = gradeAnswers([0,2,1], sampleQs);
if(r.score!==3||r.percent!==100) throw new Error(JSON.stringify(r));
});
test("all wrong = 0%", () => {
const r = gradeAnswers([1,0,0], sampleQs);
if(r.score!==0||r.percent!==0) throw new Error(JSON.stringify(r));
});
test("2/3 = 66%", () => {
const r = gradeAnswers([0,2,0], sampleQs);
if(r.score!==2) throw new Error(r.score);
});
test("results array length matches questions", () => {
const r = gradeAnswers([0,0,0], sampleQs);
if(r.results.length!==3) throw new Error(r.results.length);
});
test("correct flag set correctly", () => {
const r = gradeAnswers([0,2,1], sampleQs);
if(!r.results.every(x=>x.correct)) throw new Error("not all correct");
});
console.log(\`
\${pass} passed, \${fail} failed\`);
5. The Interface
Input: one number per question (1–4), entered at the terminal.
Output: each question graded (✓/✗), wrong answers shown with the correct one, final score and percentage.
6. Run It
node quiz.mjs # run the interactive quiz
node quiz.test.mjs # run the tests (non-interactive)🎯 Try this next
- Shuffle the questions using the Fisher-Yates algorithm so each run is different
- Add a time limit per question using
setTimeout+Promise.race - Save the high score to a JSON file using
fs.writeFile - Build a browser version — render questions as HTML, accept clicks instead of keypresses
What you practised
Three-layer architecture (data / engine / presentation), async terminal input with
readline/promises, input validation with a while loop,
array of objects as a data structure, and exporting pure functions for isolated testing.
Reference: Arrays · Objects · Async & Promises