ThumbAPI logoThumbAPI
Tutorial

Generate YouTube Thumbnails with JavaScript and Node.js

Aldin Kozica
Aldin Kozica
4 min read
Generate YouTube Thumbnails with JavaScript and Node.js — ThumbAPI Blog

Most of the JS code I write against ThumbAPI lives in three places: a Node script that runs after I push a markdown file to my blog, a small CMS plugin a client uses to generate cover art on publish, and a one-off batch runner I keep on my desktop. The snippets below are the trimmed-down versions of those, with the stuff that only matters to my setup ripped out so you can paste them in and have something working in a few minutes.

Prerequisites

  • Node.js 18 or higher (native fetch is available without extra packages)
  • A ThumbAPI API key (get one free here)

No additional packages required. The examples below use Node's built-in fetch and fs modules.

Single Thumbnail — Minimal Example

The smallest useful version. One title goes in, one WebP lands on disk.

import fs from "fs";

const API_KEY = "your_api_key_here";

async function generateThumbnail(title) {
  const response = await fetch("https://api.thumbapi.dev/v1/generate", {
    method: "POST",
    headers: {
      "x-api-key": API_KEY,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      title,
      format: "youtube",
      imageStyle: "faceless",
      outputFormat: "webp"
    })
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status} ${response.statusText}`);
  }

  const data = await response.json();
  const base64 = data.image.split(",")[1];
  const buffer = Buffer.from(base64, "base64");

  fs.writeFileSync("thumbnail.webp", buffer);
  console.log(`Saved — ${data.dimensions.width}x${data.dimensions.height}`);
}

generateThumbnail("How the Roman Empire Actually Collapsed");

Using a Custom Asset Dataset

If you've uploaded a style reference dataset to ThumbAPI, pass the asset ID to keep every thumbnail visually consistent with your channel:

async function generateThumbnail(title, customAssetsId = null) {
  const body = {
    title,
    format: "youtube",
    imageStyle: "faceless",
    outputFormat: "webp"
  };

  if (customAssetsId) {
    body.customAssetsId = customAssetsId;
  }

  const response = await fetch("https://api.thumbapi.dev/v1/generate", {
    method: "POST",
    headers: {
      "x-api-key": API_KEY,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(body)
  });

  return await response.json();
}

// With dataset
const result = await generateThumbnail(
  "How the Roman Empire Actually Collapsed",
  "m6XhjtZNdF0N2AFXUOiq"
);

Batch Generation from an Array

Processing a list of titles sequentially with a delay to respect rate limits:

import fs from "fs";
import path from "path";

const API_KEY = "your_api_key_here";
const OUTPUT_DIR = "./thumbnails";
const DELAY_MS = 2000;

if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR);

const videos = [
  { title: "How the Roman Empire Actually Collapsed", format: "youtube" },
  { title: "The Deepest Cave Ever Explored", format: "youtube" },
  { title: "Why the Stock Market Crashes Every Decade", format: "youtube" },
];

function slugify(text) {
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 60);
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function generateOne(title, format, index) {
  const response = await fetch("https://api.thumbapi.dev/v1/generate", {
    method: "POST",
    headers: {
      "x-api-key": API_KEY,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      title, format, imageStyle: "faceless", outputFormat: "webp"
    })
  });

  if (!response.ok) throw new Error(`${response.status}`);

  const data = await response.json();
  const buffer = Buffer.from(data.image.split(",")[1], "base64");
  const filename = `${String(index).padStart(3, "0")}-${slugify(title)}.webp`;
  fs.writeFileSync(path.join(OUTPUT_DIR, filename), buffer);
  return filename;
}

async function runBatch() {
  console.log(`Starting batch — ${videos.length} thumbnails`);
  for (let i = 0; i < videos.length; i++) {
    const { title, format } = videos[i];
    try {
      const filename = await generateOne(title, format, i + 1);
      console.log(`  Saved: ${filename}`);
    } catch (err) {
      console.error(`  Failed: ${err.message}`);
    }
    if (i < videos.length - 1) await sleep(DELAY_MS);
  }
}

runBatch();

Returning Base64 Directly (No File Write)

When you're integrating thumbnail generation into an API response or a CMS pipeline, you might not want to write to disk at all. Return the base64 string directly and pass it downstream:

async function getThumbnailBase64(title) {
  const response = await fetch("https://api.thumbapi.dev/v1/generate", {
    method: "POST",
    headers: {
      "x-api-key": API_KEY,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      title,
      format: "youtube",
      imageStyle: "faceless",
      outputFormat: "webp"
    })
  });

  const data = await response.json();
  return data.image; // Full data URI, ready for <img src> or upload
}

// Use in an Express handler
app.post("/generate-thumbnail", async (req, res) => {
  const { title } = req.body;
  const imageDataUri = await getThumbnailBase64(title);
  res.json({ thumbnail: imageDataUri });
});

TypeScript Version

If your project uses TypeScript, here's a typed implementation:

interface ThumbnailRequest {
  title: string;
  format: "youtube" | "blogpost" | "instagram" | "x";
  imageStyle: "faceless" | "with-image" | "with-logo";
  outputFormat?: "webp" | "png";
  customAssetsId?: string;
}

interface ThumbnailResponse {
  image: string;
  format: string;
  dimensions: {
    width: number;
    height: number;
  };
}

async function generateThumbnail(
  params: ThumbnailRequest
): Promise<ThumbnailResponse> {
  const response = await fetch("https://api.thumbapi.dev/v1/generate", {
    method: "POST",
    headers: {
      "x-api-key": process.env.THUMBAPI_KEY!,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(params)
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`ThumbAPI error ${response.status}: ${error}`);
  }

  return response.json() as Promise<ThumbnailResponse>;
}

Error Handling Reference

HTTP StatusMeaningWhat to do
200SuccessDecode and use the image
401Invalid API keyCheck your x-api-key header
422Invalid parametersCheck format and imageStyle values
429Rate limit hitAdd a delay and retry
500Server errorRetry after a short wait

That's the full shape of it. The single-thumbnail function is what I reach for inside an Express handler or a Next.js route; the batch loop is what I run from my terminal when a client sends me a CSV. If you're hitting 429s in a tight loop, bump DELAY_MS to 3000 before adding any retry logic, that fixes it nine times out of ten.

Aldin Kozica

Written by

Aldin Kozica

Full-stack developer from Bosnia and Herzegovina. I built ThumbAPI because I kept watching content teams waste hours on thumbnail design when the patterns are predictable enough to automate. The API is the tool I wished existed when building content pipelines for my own projects.

Generate Thumbnails With an API

Try ThumbAPI free. 5 thumbnail generations per month, no credit card required. One API call, production-ready output.