Command Palette
Search for a command to run...
Build a Polish quiz app#
A complete walkthrough. By the end you have a SvelteKit app that serves Polish trivia with score tracking, a per-question timer, and visible attribution. Total code: ~120 lines.
Works with any framework — the QuizBase calls are framework-agnostic.
1. Fetch from your server#
Never put qb_sk_* in client bundles. Hide the call behind your own endpoint.
// src/routes/api/round/+server.ts
import { json } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
export async function GET() {
const url = new URL('https://quizbase.runriva.com/api/v1/questions/random');
url.searchParams.set('amount', '10');
url.searchParams.set('lang', 'pl');
url.searchParams.set('type', 'multiple');
url.searchParams.set('difficulty', 'medium');
const res = await fetch(url, {
headers: { 'X-API-Key': env.QUIZBASE_KEY }
});
if (!res.ok) {
return json({ error: 'upstream' }, { status: 502 });
}
const { data, meta } = await res.json();
return json({ questions: data, requestId: meta.requestId });
} 2. Shuffle answers in the component#
QuizBase returns correctAnswer + incorrectAnswers separately. Shuffle once per render to avoid the correct answer always being in the same slot.
// src/lib/quiz.ts
export type Question = {
id: string;
text: string;
correctAnswer: string;
incorrectAnswers: string[];
attribution: { author: string; source: string; license: string; url: string };
};
export function prepareChoices(q: Question): Array<{ text: string; correct: boolean }> {
const choices = [
{ text: q.correctAnswer, correct: true },
...q.incorrectAnswers.map((t) => ({ text: t, correct: false }))
];
// Fisher-Yates in place
for (let i = choices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[choices[i], choices[j]] = [choices[j]!, choices[i]!];
}
return choices;
} 3. UI — one question at a time#
<!-- src/routes/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { prepareChoices, type Question } from '$lib/quiz';
let questions: Question[] = $state([]);
let index = $state(0);
let score = $state(0);
let picked = $state<string | null>(null);
let secondsLeft = $state(15);
const current = $derived(questions[index]);
const choices = $derived(current ? prepareChoices(current) : []);
onMount(async () => {
const res = await fetch('/api/round');
const body = await res.json();
questions = body.questions;
startTimer();
});
function startTimer() {
secondsLeft = 15;
const handle = setInterval(() => {
if (--secondsLeft <= 0) {
clearInterval(handle);
next();
}
}, 1000);
}
function pick(text: string, correct: boolean) {
picked = text;
if (correct) score++;
}
function next() {
picked = null;
if (index < questions.length - 1) {
index++;
startTimer();
}
}
</script>
{#if current}
<article>
<header>
<p>Pytanie {index + 1} / {questions.length} · {secondsLeft}s · wynik {score}</p>
</header>
<h1>{current.text}</h1>
<ul>
{#each choices as choice (choice.text)}
<li>
<button
disabled={picked !== null}
class:correct={picked && choice.correct}
class:wrong={picked === choice.text && !choice.correct}
onclick={() => pick(choice.text, choice.correct)}
>
{choice.text}
</button>
</li>
{/each}
</ul>
{#if picked !== null}
<button onclick={next}>Dalej →</button>
{/if}
<!-- Attribution — required by CC-BY-SA chain -->
<footer>
<small>
Źródło: <a href={current.attribution.url}>{current.attribution.source}</a>
· Licencja {current.attribution.license}
</small>
</footer>
</article>
{/if} 4. Edge cases#
- Empty
data[]— your filters matched nothing. Broadendifficultyor dropcategory. - Duplicate questions across rounds — pass
exclude=<comma-separated-ids>(max 100). Persist shown ids per session. - Rate limited (429) — cache rounds server-side (
Cache-Control: private, max-age=300) or pre-fetch batches.
5. Deploy#
- Put
QUIZBASE_KEYin your hosting provider’s env (Railway, Vercel, Fly, Cloudflare). - Use
qb_pk_*locally, swap forqb_sk_*in production. - Add
Cache-Control: public, max-age=300on/api/roundif acceptable.
See also#
- GET /v1/questions/random — all filters
- Rate limits in practice — caching patterns
- Languages and translations — Polish coverage and strict mode