DEV Community

Mark
Mark

Posted on Originally published at covergrabber.com

I Built a YouTube Thumbnail Downloader With Zero Backend — Here's the Trick

I Built a YouTube Thumbnail Downloader With Zero Backend — Here's the Trick
Enter fullscreen mode Exit fullscreen mode

A while back I needed a YouTube video's cover image for a presentation. I hit the usual wall: sketchy "downloader" sites full of redirects, browser extensions asking for permissions they had no business asking for, and the official Data API demanding a key and a quota just to fetch one image.

So I built CoverGrabber — a small tool that does the lookup for you. No signup, no API key, no server at all, actually. The whole thing runs client-side. Here's how.

The trick: YouTube's thumbnail URLs are predictable

Every YouTube video already has its thumbnails sitting on a public CDN at a fixed URL pattern:

https://img.youtube.com/vi/{VIDEO_ID}/maxresdefault.jpg
https://img.youtube.com/vi/{VIDEO_ID}/sddefault.jpg
https://img.youtube.com/vi/{VIDEO_ID}/hqdefault.jpg
https://img.youtube.com/vi/{VIDEO_ID}/mqdefault.jpg
https://img.youtube.com/vi/{VIDEO_ID}/default.jpg
Enter fullscreen mode Exit fullscreen mode

No auth, no API, no quota. If you have the 11-character video ID, you have every thumbnail size that exists for it. The "hard" part isn't fetching the image, it's figuring out the ID and knowing which of those five sizes actually exist.

Step 1: pulling the ID out of any URL format

People paste links in every shape imaginable: watch?v=, youtu.be/, /shorts/, /embed/. One regex handles all of them:

function extractId(url) {
  const m = url.match(/(?:v=|\/shorts\/|\/embed\/|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
  return m ? m[1] : null;
}
Enter fullscreen mode Exit fullscreen mode

Video IDs are always exactly 11 characters of [A-Za-z0-9_-], so that's the one invariant I could anchor on regardless of which URL shape shows up.

Step 2: the part that actually tripped me up

Not every video has a maxresdefault.jpg. Smaller or older uploads sometimes only go up to sddefault or hqdefault. My first instinct was to just try loading the image and catch a 404 with onerror.

That doesn't work. YouTube never 404s on these URLs. If a size doesn't exist, it silently returns a 120×90 grey placeholder image instead of an error. Which means onerror never fires, and you end up rendering a tiny grey box as if it were a real thumbnail.

The fix: load it anyway, then check the actual dimensions of what came back.

function checkImage(src) {
  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => {
      // YouTube serves a 120x90 grey placeholder when a size
      // doesn't exist for this video — that's the tell.
      resolve(!(img.naturalWidth === 120 && img.naturalHeight === 90));
    };
    img.onerror = () => resolve(false);
    img.src = src;
  });
}
Enter fullscreen mode Exit fullscreen mode

If naturalWidth/naturalHeight come back as exactly 120×90, it's the placeholder, not a real thumbnail, so I just skip that size in the results grid. This one check is basically the whole reason the tool feels reliable instead of showing broken/empty boxes half the time.

Step 3: checking all five sizes in parallel

const SIZES = ['maxresdefault', 'sddefault', 'hqdefault', 'mqdefault', 'default'];

const results = await Promise.all(
  SIZES.map(async (size) => {
    const src = `https://img.youtube.com/vi/${id}/${size}.jpg`;
    const ok = await checkImage(src);
    return { size, src, ok };
  })
);

results.filter(r => r.ok).forEach(renderThumbnail);
Enter fullscreen mode Exit fullscreen mode

Five image loads, checked concurrently, filtered down to whatever's real. That's the entire backend. There isn't one.

Downloading without a server

The last piece was making "Download" actually download instead of just opening the image in a new tab. Since the images are cross-origin, I fetch them as a blob and trigger the save through an object URL, falling back to window.open if a browser blocks it:

async function downloadImage(url, filename) {
  try {
    const res = await fetch(url, { mode: 'cors' });
    const blob = await res.blob();
    const blobUrl = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = blobUrl;
    a.download = filename;
    a.click();
    URL.revokeObjectURL(blobUrl);
  } catch {
    window.open(url, '_blank'); // graceful fallback
  }
}
Enter fullscreen mode Exit fullscreen mode

What it turned into

That core trick ended up being reusable enough that I built a couple more tools around it, a thumbnail resizer and a banner resizer, both using the Canvas API for the drag-to-crop part instead of pulling in a library. Same philosophy throughout: no backend, nothing uploaded anywhere, everything happens in the tab you're already in.

You can try the original tool here: covergrabber.com

Feedback welcome

This is a side project I'm actively iterating on. If you've dealt with the same "grab this public image without spinning up a backend" problem for something else, I'd genuinely like to hear how you approached it. And if you spot an edge case where the 120×90 check would misfire, drop it in the comments, that's exactly the kind of thing I want to know about.

Top comments (0)