Guides / Tutorials

Build a dog breed quiz with real breed images

By the Dog Image API team. Updated .

This quiz is one HTML file with no build step. It loads the breed list once, picks ten random breeds in the browser, shows each head illustration with four name choices, and keeps score. Save the code below as quiz.html, open it in a browser, and it works.

What it does

The API has no random endpoint and no search, so the browser does the choosing. Here is the plan:

  1. Fetch GET /api/v1/breeds. It needs no key and cross-origin reads from other sites are allowed.
  2. Drop the entries where crossbreed is true. A mix has no single look to recognize.
  3. Shuffle what is left and take the first ten as the answers.
  4. For each answer, pick three other breeds as wrong choices and shuffle the four names.
  5. Show the answer's head illustration from GET /api/v1/images/{breed}/{variant}/head?size=512. This URL redirects to a free WebP preview, and an <img> tag follows the redirect.

The images are AI-generated illustrations of an adult dog, one set per breed. For breeds with several colors the quiz uses the default_variant from the list, so it does not need a second request.

The complete file

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Dog breed quiz</title>
  <style>
    body { font-family: system-ui, sans-serif; background: #f6f1e7; color: #1d1b16; margin: 0; }
    main { max-width: 28rem; margin: 0 auto; padding: 1.5rem 1rem 3rem; }
    h1 { font-size: 1.5rem; margin: 0 0 1rem; }
    #dog { display: block; width: 100%; max-width: 20rem; aspect-ratio: 1; margin: 1rem auto;
           background: #fff; border: 2px solid #1d1b16; border-radius: 12px; }
    #choices { display: grid; gap: 0.5rem; }
    button { font: inherit; padding: 0.7rem 1rem; border: 2px solid #1d1b16; border-radius: 8px;
             background: #fff; cursor: pointer; text-align: left; }
    button:disabled { cursor: default; }
    button.right { background: #c9efc9; }
    button.wrong { background: #f4c7c3; }
    #next, #again { margin-top: 1rem; background: #ffd966; }
    .credit { font-size: 0.85rem; margin-top: 2rem; }
  </style>
</head>
<body>
<main>
  <h1>Dog breed quiz</h1>
  <p id="status">Loading breeds...</p>

  <div id="quiz" hidden>
    <p id="score"></p>
    <img id="dog" alt="AI-generated illustration of a dog. Which breed is it?">
    <div id="choices"></div>
    <p id="feedback" aria-live="polite"></p>
    <button id="next" hidden>Next</button>
  </div>

  <div id="done" hidden>
    <p id="final"></p>
    <button id="again">Play again</button>
  </div>

  <p class="credit">
    Images are AI-generated illustrations, not photos.
    <a href="https://dogimage.dev">Dog images: Dog Image API</a>
  </p>
</main>

<script>
const BASE = "https://dogimage.dev";
const ROUNDS = 10;
const VIEW = "head"; // try "front-left" for a full-body picture

const $ = (id) => document.getElementById(id);
let breeds = [];
let questions = [];
let round = 0;
let score = 0;

function shuffle(list) {
  const a = [...list];
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

function imageUrl(breed) {
  return `${BASE}/api/v1/images/${breed.slug}/${breed.default_variant}/${VIEW}?size=512`;
}

function buildQuestions() {
  return shuffle(breeds).slice(0, ROUNDS).map((answer) => {
    const wrong = shuffle(breeds.filter((b) => b.slug !== answer.slug)).slice(0, 3);
    return { answer, options: shuffle([answer, ...wrong]) };
  });
}

function showQuestion() {
  const q = questions[round];
  $("score").textContent = `Question ${round + 1} of ${ROUNDS}. Score: ${score}`;
  $("dog").src = imageUrl(q.answer);
  $("feedback").textContent = "";
  $("next").hidden = true;
  const box = $("choices");
  box.replaceChildren();
  for (const option of q.options) {
    const btn = document.createElement("button");
    btn.textContent = option.name;
    btn.addEventListener("click", () => answer(option, btn));
    box.appendChild(btn);
  }
}

function answer(option, clicked) {
  const q = questions[round];
  const correct = option.slug === q.answer.slug;
  if (correct) score++;
  for (const btn of $("choices").children) {
    btn.disabled = true;
    if (btn.textContent === q.answer.name) btn.classList.add("right");
  }
  if (!correct) clicked.classList.add("wrong");

  const feedback = $("feedback");
  feedback.textContent = correct ? "Correct. " : `It was the ${q.answer.name}. `;
  const link = document.createElement("a");
  link.href = `${BASE}/dogs/${q.answer.slug}`;
  link.textContent = `${q.answer.name} image: Dog Image API`;
  feedback.appendChild(link);

  $("score").textContent = `Question ${round + 1} of ${ROUNDS}. Score: ${score}`;
  $("next").textContent = round + 1 < ROUNDS ? "Next" : "See result";
  $("next").hidden = false;
}

function finish() {
  $("quiz").hidden = true;
  $("done").hidden = false;
  $("final").textContent = `You got ${score} out of ${ROUNDS}.`;
}

function startGame() {
  questions = buildQuestions();
  round = 0;
  score = 0;
  $("done").hidden = true;
  $("quiz").hidden = false;
  showQuestion();
}

$("next").addEventListener("click", () => {
  round++;
  if (round < ROUNDS) showQuestion();
  else finish();
});
$("again").addEventListener("click", startGame);

async function init() {
  try {
    const res = await fetch(`${BASE}/api/v1/breeds`);
    if (!res.ok) throw new Error(`Breed list failed with status ${res.status}`);
    const data = await res.json();
    breeds = data.breeds.filter((b) => !b.crossbreed);
    $("status").hidden = true;
    startGame();
  } catch (err) {
    $("status").textContent = "Could not load the breed list. Reload to try again.";
    console.error(err);
  }
}

init();
</script>
</body>
</html>

How the pieces fit

Choices. buildQuestions shuffles the whole list once, so the ten answers never repeat. For each answer it shuffles the other breeds and takes three. The check on slug guarantees four different names.

The image. The quiz sets img.src to the image endpoint with size=512. That endpoint answers with a 302 to the preview file, which is cached for a year. The alt text describes the task and does not give away the answer.

Credit. Using previews requires a visible link that credits Dog Image API next to the image or on the same page. The footer link covers the whole page. After each answer the quiz also shows the breed-specific credit link in the form each breed page gives. Putting that link before the answer would reveal the name, which is why it appears afterward.

Errors. If the list fails to load, the page says so and logs the error to the console. It does not retry in a loop.

Ideas to extend it

  • Change VIEW to front-left for a full-body round, or choose a view at random per question from the nine ids in the views array of the breed list.
  • Keep the best score in localStorage.
  • Link each answer to its breed page or to the side-view gallery for further browsing.
  • Read the variants list to add a "which color is this?" round for the seven breeds with several colors.

The quiz never uses a key and never touches the full-size endpoint. For production use, put the file on any static host. The docs list every endpoint if you want to go beyond previews.

Questions

Is there a random image endpoint?

No. The API has no random endpoint. This quiz loads GET /api/v1/breeds once and picks breeds in the browser.

Do I need an API key for the quiz?

No. The breed list and the 512 pixel previews need no key, and browsers on other sites may read them. A key is only for full-size downloads, and those belong on a server.

Are these real dog photos?

No. They are AI-generated illustrations. The quiz says so on screen, and you should keep that line.

Why does the quiz skip crossbreeds?

A mix such as the cockapoo has no single recognizable look, so it makes a poor quiz answer. The crossbreed field lets the quiz filter them out in one line.

Next steps