Errors, limits & best practices
What happens when something goes wrong, what we saw in our own tests, and simple code that keeps your app steady. Anything we could not test yourself is marked clearly.
Rule 1 · Treat “empty” as normal
A successful response can still contain nothing to show. Check the length before you render, and show a message that explains what happened.
Rule 2 · Check the response, not just the network
Some failures return HTML or a fallback object with status 200. Confirm the content type and the expected fields before you trust the data.
Rule 3 · Validate input before the call
Video IDs must be 11 characters from A-Z a-z 0-9 _ -. Search text must not be empty. Clamp limit to 1 to 50.
Rule 4 · Be polite to upstream
There is no official quota, but bursts of requests can trigger CAPTCHAs or 429 responses. Cache, space out bulk work and retry with backoff.
Safe client code
A small wrapper that validates input, checks the response type, retries transient failures, and always returns a predictable shape. Uses only fetch and AbortController, so it runs in modern browsers and in Node.js.
const API = 'https://ytapis.djalokyt27.workers.dev';
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
export async function ytapiGet(path, { retries = 3, timeoutMs = 15000 } = {}) {
for (let attempt = 0; ; attempt++) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(API + path, { signal: ctrl.signal });
const type = res.headers.get('content-type') || '';
if (res.status === 429 || res.status >= 500) throw new Error('retryable ' + res.status);
if (!res.ok) return { ok: false, error: `HTTP ${res.status}`, data: null };
if (!type.includes('application/json')) return { ok: false, error: 'Not JSON', data: null };
return { ok: true, data: await res.json(), error: null };
} catch (err) {
if (attempt >= retries) return { ok: false, error: err.message, data: null };
const delay = Math.min(8000, 500 * 2 ** attempt) * (0.5 + Math.random() / 2);
await sleep(delay);
} finally {
clearTimeout(timer);
}
}
}
// Usage: always check ok and the length before rendering
const r = await ytapiGet('/?q=cats&limit=5');
const videos = r.ok && Array.isArray(r.data) ? r.data : [];
if (videos.length === 0) console.log('No results right now');Note: the wrapper does not retry a 400 or 404, because those will not succeed on a second try.
Caching
Cache by URL with a time limit. Five to ten minutes is a good default for search and channel data. Stats and live status change faster, so cache them for about a minute.
const cache = new Map();
async function cached(path, ttlMs = 5 * 60 * 1000) {
const hit = cache.get(path);
if (hit && Date.now() - hit.at < ttlMs) return hit.value;
const value = await ytapiGet(path);
cache.set(path, { at: Date.now(), value });
return value;
}Bulk jobs
Run requests one after another with a short pause, not all at once. Half a second between calls is a polite default. Save results as you go so a failure does not lose the whole batch.
const ids = ['dQw4w9WgXcQ', 'jfKfPfyJRdk'];
for (const id of ids) {
const r = await ytapiGet(`/video/${id}/stats`);
if (r.ok) console.log(id, r.data.views);
await sleep(500);
}Health check before a batch job
Start every job with /health. If it does not answer with {"status":"ok"}, stop and try again later instead of retrying every request.
curl -fsS 'https://ytapis.djalokyt27.workers.dev/health' || echo "API is not reachable"Found a bug or a changed response?
Open an issue on GitHub with the URL you called and the response you got. Upstream changes are the most common cause of empty or broken data.