Skip to content
Command Palette
Search for a command to run...
QuizBase · Docs

Build a multi-round quiz#

This guide walks through everything you need to build a real quiz app on top of QuizBase: discover what’s available, plan rounds by sub-topic, fetch questions without repeats, and ship it.

We’ll build a Star Wars quiz with three rounds:

  1. Round 1 — General Star Wars (any difficulty, mixed topics)
  2. Round 2 — Films and franchises (focused on science-fiction-films subcategory)
  3. Round 3 — Characters (Darth Vader, Skywalker, etc.)

Step 1 — Discover what exists#

The first question for any new dataset: what topics are even there? QuizBase exposes a curated topic layer (~2,000 entries) with aliases and counts.

The response gives us slug: 'star-wars' and count: 487 — enough material for several rounds.

Step 2 — Inspect facets#

Before writing the round logic, fetch the topic detail to see what difficulty mix and sub-topics live inside.

Now we have the data to size the rounds:

  • Round 1 — 10 random Star Wars questions (mixed)
  • Round 2 — 10 from the science-fiction-films subcategory (380 to choose from)
  • Round 3 — 10 with darth-vader or skywalker tags (OR logic)

Step 3 — Fetch each round, exclude what was seen#

QuizBase doesn’t keep server-side state for your quiz session. Instead, track question IDs in your client and pass them as ?exclude=<csv> (max 250) on each subsequent call. This guarantees no repeats across rounds.

const apiKey = process.env.QUIZBASE_API_KEY!;
const headers = { 'X-API-Key': apiKey };
const seen = new Set<string>();

async function fetchRound(params: URLSearchParams): Promise<Question[]> {
  if (seen.size > 0) params.set('exclude', [...seen].join(','));
  const res = await fetch(`https://quizbase.runriva.com/api/v1/questions/random?${params}`, {
    headers
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const { data } = await res.json() as { data: Question[] };
  for (const q of data) seen.add(q.id);
  return data;
}

// Round 1 — general Star Wars
const round1 = await fetchRound(new URLSearchParams({
  topic: 'star-wars',
  amount: '10',
  lang: 'en'
}));

// Round 2 — films and franchises
const round2 = await fetchRound(new URLSearchParams({
  topic: 'star-wars',
  subcategory: 'science-fiction-films',
  amount: '10',
  lang: 'en'
}));

// Round 3 — characters (OR logic across two tags)
const round3 = await fetchRound(new URLSearchParams({
  tags_any: 'darth-vader,skywalker',
  amount: '10',
  lang: 'en'
}));

Step 4 — Render and play#

Each question comes with correctAnswer, incorrectAnswers[], attribution, and subcategories/tags arrays you can use for hints. Shuffle answers client-side and you’re done.

function shuffle<T>(arr: T[]): T[] {
  return [...arr].sort(() => Math.random() - 0.5);
}

for (const q of round1) {
  const choices = shuffle([q.correctAnswer, ...q.incorrectAnswers]);
  console.log(q.text);
  for (const c of choices) console.log(`  - ${c}`);
}

Tips for production quizzes#

  • Always set a lang. Don’t rely on the default — make sure your UI matches.
  • Use ?quality=high to skip questions flagged for review (needs_review=true). Trades dataset size for distractor quality.
  • Set ?has_explanation=true if your UX shows “why this answer” panels.
  • Cap exclude carefully. 250 IDs covers ~25 rounds of 10. For longer sessions, refresh per category or rotate seen pools.
  • Cache /topics and /categories aggressively. Both are public, no API key needed, and only change when we update content.

See also#