Guides / Tutorials
Generate a typed client from the OpenAPI file
By the Dog Image API team. Updated .
Run openapi-typescript on https://dogimage.dev/openapi.json to get TypeScript types for every path, reply and error. Then write a small fetch wrapper that uses those types. You end up with a client where a wrong view name or size fails at compile time, with no SDK to install.
What the file contains
Read it before you generate anything, so you know what the types cover. The file is OpenAPI 3.1.0, titled "Dog Image API", version 1.0.0, with one server: the production URL. It has four operations:
| operationId | Path | Notes in the file |
|---|---|---|
listBreeds |
/api/v1/breeds |
200 returns a BreedList |
getBreed |
/api/v1/breeds/{breed} |
200 returns a BreedDetail, 404 returns the shared Error |
getImage |
/api/v1/images/{breed}/{variant}/{view} |
view is an enum of the nine ids, size is 512, 1024 or full with default 1024; replies 200, 302, 400, 401, 402, 404, 429, 502, 503 |
getUsage |
/api/v1/usage |
needs the key; returns a Usage |
The schemas are BreedList, BreedSummary, VariantSummary, View, BreedDetail, VariantDetail, ViewLinks, Usage and Error. The Error schema has an error object with a code, a message and an optional choices list for the replies that offer valid values. The full-size 200 reply declares the X-Quota-Limit, X-Quota-Used and X-Quota-Remaining headers. One security scheme, key, is declared as HTTP bearer. The image path lists security as optional or key, which matches the product: previews need no key, and size=full does.
Generate the types
npm install --save-dev openapi-typescript typescript
npx openapi-typescript https://dogimage.dev/openapi.json -o schema.d.ts
The command reads the live file and writes schema.d.ts. Commit it, and run the command again when you want to pick up changes. The generated paths interface gives you, for the image route, a path object with breed, variant and a view union, and a query object whose size is "512" | "1024" | "full". The components interface gives you BreedList, BreedDetail, Usage and Error, and the operations interface holds the four operations by id.
A typed wrapper
Save this as dog.ts next to schema.d.ts. It has no dependencies other than Node 20 or newer, or any runtime with a global fetch. Every reply type comes from the generated file.
import type { components, paths } from "./schema";
type ImageGet = paths["/api/v1/images/{breed}/{variant}/{view}"]["get"];
export type ImageParams = ImageGet["parameters"]["path"];
export type View = ImageParams["view"];
export type PreviewSize = "512" | "1024";
type ApiError = components["schemas"]["Error"];
export type BreedList = components["schemas"]["BreedList"];
export type BreedDetail = components["schemas"]["BreedDetail"];
export type Usage = components["schemas"]["Usage"];
export class DogImageApiError extends Error {
constructor(
readonly status: number,
readonly code: string,
message: string,
) {
super(`${status} ${code}: ${message}`);
}
}
const BASE = "https://dogimage.dev";
async function toError(res: Response): Promise<DogImageApiError> {
const body = (await res.json().catch(() => null)) as ApiError | null;
return new DogImageApiError(
res.status,
body?.error?.code ?? "unknown",
body?.error?.message ?? res.statusText,
);
}
async function getJson<T>(path: string, key?: string): Promise<T> {
const res = await fetch(BASE + path, {
headers: key ? { Authorization: `Bearer ${key}` } : {},
});
if (!res.ok) throw await toError(res);
return (await res.json()) as T;
}
export const listBreeds = () => getJson<BreedList>("/api/v1/breeds");
export const getBreed = (breed: string) =>
getJson<BreedDetail>(`/api/v1/breeds/${encodeURIComponent(breed)}`);
export const getUsage = (key: string) => getJson<Usage>("/api/v1/usage", key);
export async function fetchFullImage(
key: string,
{ breed, variant, view }: ImageParams,
): Promise<{ bytes: ArrayBuffer; quotaRemaining: string | null }> {
const url = `${BASE}/api/v1/images/${breed}/${variant}/${view}?size=full`;
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
if (!res.ok) throw await toError(res);
return {
bytes: await res.arrayBuffer(),
// Declared as a header of the 200 reply. "unlimited" on the All images plan.
quotaRemaining: res.headers.get("X-Quota-Remaining"),
};
}
export function previewUrl(params: ImageParams, size: PreviewSize = "1024"): string {
const { breed, variant, view } = params;
return `${BASE}/api/v1/images/${breed}/${variant}/${view}?size=${size}`;
}
How the types help
ImageParams is read straight from the generated paths type, so it stays in step with the file. If you pass view: "side", the compiler stops you, because the file's enum lists only front, front-left, left, rear-left, rear, rear-right, right, front-right and head. The variant is a plain string, because the file describes it as "a color slug, or default". The Usage type has limit and remaining as number | null, which is exactly how the All images plan shows up.
toError still catches a failed JSON parse. That is good practice for any HTTP client, because a proxy, a captive portal or a network failure can hand you an error page that the API never wrote. When that happens you get the status and the status text instead of a parse error.
Use it
import { DogImageApiError, fetchFullImage, getBreed, getUsage, listBreeds } from "./dog";
import { writeFile } from "node:fs/promises";
const key = process.env.DOG_IMAGE_API_KEY;
if (!key) throw new Error("Set DOG_IMAGE_API_KEY");
const { breeds } = await listBreeds();
console.log(`${breeds.length} breeds`);
const pug = await getBreed("pug");
console.log(pug.name, pug.variants.map((v) => v.slug));
console.log(await getUsage(key));
try {
const { bytes, quotaRemaining } = await fetchFullImage(key, {
breed: "pug",
variant: "default",
view: "front",
});
await writeFile("pug-front.png", Buffer.from(bytes));
console.log("saved pug-front.png, remaining:", quotaRemaining);
} catch (err) {
if (err instanceof DogImageApiError && err.code === "quota_used_up") {
console.error("Monthly limit reached");
} else {
throw err;
}
}
Run it with your usual TypeScript setup, such as tsc, a bundler, or a runner like tsx. It uses top-level await, so the module setting needs to allow it. previewUrl builds a link that is safe to put in an <img> tag. A displayed preview needs the credit link from the license, and the images are AI-generated illustrations.
Keep the key off the client
The generated types are fine to share, since they carry no secret. Browsers on other sites may call listBreeds and getBreed directly, and may load preview files, because cross-origin reads are allowed on those. The fetchFullImage function is not for a browser bundle, because it needs the key. Run it in a build step or on a server, and save the PNGs to your own storage. The errors and quotas reference says which codes deserve a retry.
Other generators
openapi-typescript is one choice, and the wrapper above is hand-written on top of it. Other generators exist, for example OpenAPI Generator and Orval. Whichever you pick, read its output against the schemas listed above before you rely on it. The docs page lists the endpoints in prose.
Questions
Where is the Dog Image API OpenAPI file?
At https://dogimage.dev/openapi.json. It is an OpenAPI 3.1.0 document with four operations, a bearer key security scheme and typed schemas for every reply.
Is there an official Dog Image API SDK?
No. There is no SDK. The OpenAPI file and a few lines of fetch are the supported way to build a client, and this guide shows one.
Which parts of the Dog Image API does the OpenAPI file describe?
Every reply: the breed list, one breed, the image endpoint, usage and the shared Error. It also declares the X-Quota headers on the full-size reply, so generated types cover them.
Can I put the generated client in browser code?
The breed list and detail calls can run in a browser. The full-size call needs your key, so keep it in a server or a build step.
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