Guides / Tutorials
Download dog breed images with Node.js
By the Dog Image API team. Updated .
In Node.js 20 or newer, call fetch with your key as a Bearer token, read the reply as an ArrayBuffer, and write it to disk. Check /api/v1/usage first, skip files you already saved, and stop on a 429. The script below does all three with no dependencies.
Why skip files you already have
The API counts distinct full-size images per calendar month (UTC), so asking for the same image again in the same month costs nothing against your plan. It still costs time and bandwidth, because each file is a 2048 x 2048 PNG of roughly 1 to 7 MB. A file check on disk makes a re-run finish in seconds. It also protects you in a new month, when a repeat fetch would count again.
Prepare
Put the key in the environment. The key is created on the account page after sign-in, and it must stay on the server side.
export DOG_IMAGE_API_KEY="paste-your-key-here"
node --version
The second line should print v20 or higher.
The script
Save this as download.mjs. The .mjs extension lets you use import and top-level await without a package.json.
import { access, mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
const BASE = "https://dogimage.dev";
const key = process.env.DOG_IMAGE_API_KEY;
if (!key) {
console.error("Set DOG_IMAGE_API_KEY first.");
process.exit(1);
}
const auth = { Authorization: `Bearer ${key}` };
const breeds = ["golden-retriever", "german-shepherd", "dachshund", "samoyed"];
const views = ["front", "left", "head"];
const variant = "default";
const outDir = "dogs";
async function describe(res) {
const body = await res.json().catch(() => null);
const err = body?.error;
return err
? `${res.status} ${err.code}: ${err.message}`
: `${res.status} ${res.statusText}`;
}
async function exists(file) {
try {
await access(file);
return true;
} catch {
return false;
}
}
async function getUsage() {
const res = await fetch(`${BASE}/api/v1/usage`, { headers: auth });
if (!res.ok) throw new Error(await describe(res));
return res.json();
}
async function fetchFull(breed, view) {
const url = `${BASE}/api/v1/images/${breed}/${variant}/${view}?size=full`;
for (let attempt = 1; attempt <= 3; attempt++) {
const res = await fetch(url, { headers: auth });
if (res.ok) return Buffer.from(await res.arrayBuffer());
if (res.status === 429) return null; // quota is spent, tell the caller to stop
if ((res.status === 502 || res.status === 503) && attempt < 3) {
await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
continue;
}
throw new Error(await describe(res));
}
}
// 1. Build the list of files that are not on disk yet.
const wanted = breeds.flatMap((breed) => views.map((view) => ({ breed, view })));
const todo = [];
for (const job of wanted) {
const file = path.join(outDir, job.breed, variant, `${job.view}.png`);
if (!(await exists(file))) todo.push({ ...job, file });
}
console.log(`${wanted.length - todo.length} already saved, ${todo.length} to fetch.`);
// 2. Check the plan before spending anything.
const usage = await getUsage();
if (usage.plan === null) {
console.error("This key has no active plan. Full-size downloads are not available.");
process.exit(1);
}
const room = usage.remaining ?? Infinity; // remaining is null on the All images plan
console.log(`Plan ${usage.plan}, month ${usage.month}: used ${usage.used}, remaining ${usage.remaining ?? "unlimited"}.`);
const batch = todo.slice(0, room);
if (batch.length < todo.length) {
console.warn(`Only ${batch.length} of ${todo.length} fit in this month's plan.`);
}
// 3. Download one at a time and write each file as soon as it arrives.
for (const { breed, view, file } of batch) {
const png = await fetchFull(breed, view);
if (png === null) {
console.error("Quota used up. Stopping. Saved files are kept.");
break;
}
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, png);
console.log("saved", file);
}
Run it with node download.mjs. Run it again and the first line reports that everything is already saved.
What to notice
The order of the three steps
The script builds the work list first, then reads usage, then downloads. The list is what you would spend, so the usage check compares like with like. A re-run builds a shorter list, and a month with fewer images left trims the batch.
The usage reply
GET /api/v1/usage returns month, plan, limit, used and remaining. On the All images plan, limit and remaining are null, which is why the script falls back to Infinity. With no plan, plan is null and limit is 0. A 401 here means the key is missing or wrong.
One caveat: the count is of distinct images, and todo is made of files missing on disk, not images missing from your quota. If you deleted a file you fetched earlier this month, the new request is free. The check errs on the safe side.
Sequential on purpose
The loop awaits each request before the next. The API states no per-request rate limit, and the quota is the real ceiling, so there is no gain in firing hundreds of requests at once. Sequential code also makes the stop on 429 exact.
Retry only what can recover
A 502 means the file was unavailable and was not counted. A 503 means the plan check failed. Both are worth a short wait and another try. The script gives up after three attempts and throws the readable message. A 404 lists the valid choices in its reply, which makes a typo in a slug easy to fix. See the errors and quotas reference for every code.
Check a breed before you ask for it
Colors vary by breed. If you want a color other than the default, read GET /api/v1/breeds/{breed} first. It needs no key and lists each variant with its views. Compare that with the catalog on the dogs page, or the left side gallery to see one angle across every breed.
After the download
Store the files and serve them from your own host. The license allows use inside your own products and forbids republishing the images as a collection. The pricing page shows what each plan includes.
Questions
Which Node.js version do I need?
Node 20 or newer. The script uses the built-in fetch and the node:fs/promises module, so it has no dependencies.
Why check /api/v1/usage before downloading?
It tells you the plan and how many full-size images remain this month. You can then trim a long list before the first request instead of hitting a 429 halfway through.
Can I call the full-size endpoint from browser code?
No. The full-size endpoint needs your key, and a key in browser code is public. Run the download in a Node script, a build step or a server.
Are the dog images photos?
No. They are AI-generated illustrations of a dog in nine standard views on a transparent background. Each set passes an automatic check before it is published.
Next steps
- API docs: endpoints, keys, limits and errors.
- All dog breeds, each with nine views.
- Pricing for full-size transparent PNG files.
- A dog breed card with Tailwind CSS
- A dog breed gallery in SvelteKit
- A dog breed picker in Angular
- A dog breed selector in Vue 3