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:
- Round 1 — General Star Wars (any difficulty, mixed topics)
- Round 2 — Films and franchises (focused on
science-fiction-filmssubcategory) - 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-filmssubcategory (380 to choose from) - Round 3 — 10 with
darth-vaderorskywalkertags (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=highto skip questions flagged for review (needs_review=true). Trades dataset size for distractor quality. - Set
?has_explanation=trueif your UX shows “why this answer” panels. - Cap
excludecarefully. 250 IDs covers ~25 rounds of 10. For longer sessions, refresh per category or rotate seen pools. - Cache
/topicsand/categoriesaggressively. Both are public, no API key needed, and only change when we update content.
See also#
- GET /v1/topics — full discovery reference
- GET /v1/questions/random — full filter list
- Rate limits and retries — production-grade fetch patterns