Use the official YouTube Data API search.list method to collect structured search results in Node.js. Send an explicit query, regionCode, relevanceLanguage, type, and bounded page size; follow nextPageToken only up to a declared page cap. A proxy is optional for a separate rendered-page QA session. It does not replace those API parameters or silently turn an API response into the result page seen by a person in that country.
This tutorial collects at most two pages of ten public video results for the query “home router setup,” aimed at Romania and Romanian-language relevance. It produces JSON with the exact request controls and normalized video identifiers. That narrow scope makes quota use, pagination, and interpretation visible instead of hiding them inside a general-purpose scraper.
Define the API observation before writing code
The official search.list reference says regionCode asks for videos viewable in the specified ISO 3166-1 alpha-2 country, while relevanceLanguage asks for results most relevant to a language and can still return highly relevant material in other languages. These are request controls, not claims that every viewer in Romania receives the same order.
The same reference currently assigns each call 1 unit in the Search Queries quota bucket and limits search.list to 100 calls per day. Each page is a call. The example’s two-page ceiling therefore uses no more than two calls and two units for one run. Check the linked reference again before scheduling work because quota systems and project allocations can change.
Set type=video so every identifier has id.videoId. Without that filter, the default response can mix videos, channels, and playlists, each with a different identifier shape. Keep the returned order only as the order for this exact request and capture time; do not label it a permanent or universal ranking.
Run a complete bounded Node.js request
Use Node.js 18 or newer so fetch is available without another package. Enable the YouTube Data API in a Google Cloud project, create a restricted API key, and set it in the environment for the current shell. Never place the key in source control or output. The Data API overview covers project setup, credentials, resources, and general quota accounting.
function buildSearchUrl({
apiKey,
query,
regionCode,
relevanceLanguage,
maxResults,
pageToken,
}) {
const params = new URLSearchParams({
part: 'snippet',
q: query,
type: 'video',
order: 'relevance',
regionCode,
relevanceLanguage,
maxResults: String(maxResults),
key: apiKey,
});
if (pageToken) params.set('pageToken', pageToken);
return `https://www.googleapis.com/youtube/v3/search?${params.toString()}`;
}
function normalizeItem(item, page, position) {
const videoId = item?.id?.videoId;
const snippet = item?.snippet;
if (!videoId || !snippet?.title || !snippet?.channelId) return null;
return {
videoId,
url: `https://www.youtube.com/watch?v=${videoId}`,
title: snippet.title,
channelId: snippet.channelId,
channelTitle: snippet.channelTitle ?? null,
publishedAt: snippet.publishedAt ?? null,
page,
position,
};
}
async function collectSearchResults({
apiKey,
query,
regionCode,
relevanceLanguage,
maxResults = 10,
maxPages = 2,
fetchImpl = fetch,
}) {
if (!apiKey) throw new Error('YOUTUBE_API_KEY is required');
if (!query.trim()) throw new Error('query must not be empty');
if (!/^[A-Z]{2}$/.test(regionCode)) throw new Error('regionCode must be two uppercase letters');
if (maxResults < 1 || maxResults > 50) throw new Error('maxResults must be between 1 and 50');
if (maxPages < 1 || maxPages > 3) throw new Error('maxPages must be between 1 and 3');
const results = [];
let pageToken = null;
let pagesFetched = 0;
while (pagesFetched < maxPages) {
const url = buildSearchUrl({
apiKey, query, regionCode, relevanceLanguage, maxResults, pageToken,
});
const response = await fetchImpl(url, { headers: { accept: 'application/json' } });
const body = await response.json().catch(() => null);
if (!response.ok) {
const reason = body?.error?.message ?? 'non-JSON API error';
throw new Error(`YouTube API ${response.status}: ${reason}`);
}
if (body?.kind !== 'youtube#searchListResponse' || !Array.isArray(body.items)) {
throw new Error('Unexpected YouTube search response shape');
}
pagesFetched += 1;
body.items.forEach((item, index) => {
const normalized = normalizeItem(item, pagesFetched, index + 1);
if (normalized) results.push(normalized);
});
pageToken = typeof body.nextPageToken === 'string' ? body.nextPageToken : null;
if (!pageToken) break;
}
return {
method: 'youtube-data-api-search-list',
query,
requestedRegionCode: regionCode,
responseRegionCode: null,
relevanceLanguage,
maxResultsPerPage: maxResults,
maxPages,
pagesFetched,
stoppedWithAnotherPageAvailable: Boolean(pageToken),
capturedAt: new Date().toISOString(),
results,
};
}
async function main() {
const output = await collectSearchResults({
apiKey: process.env.YOUTUBE_API_KEY ?? '',
query: 'home router setup',
regionCode: 'RO',
relevanceLanguage: 'ro',
maxResults: 10,
maxPages: 2,
});
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
}
main().catch((error) => {
process.stderr.write(`Search failed: ${error.message}\n`);
process.exitCode = 1;
});
Save the program as youtube-search.js. In PowerShell, set $env:YOUTUBE_API_KEY='your-restricted-key' and run node youtube-search.js. A failed HTTP status is decoded before the program exits, an unexpected response shape is rejected, and a new page is requested only when the response supplies nextPageToken. The cap remains effective even when another token exists.
The example leaves responseRegionCode as null because its small normalized return does not carry the page response after each loop. If you need YouTube’s response-level region field, add a list of observed body.regionCode values; do not copy the requested value into a response field and call it verified.
Read the JSON as a request record, not a universal rank
A shortened successful output has this shape. The timestamps and IDs below are illustrative, but every key is emitted by the program.
{
"method": "youtube-data-api-search-list",
"query": "home router setup",
"requestedRegionCode": "RO",
"responseRegionCode": null,
"relevanceLanguage": "ro",
"maxResultsPerPage": 10,
"maxPages": 2,
"pagesFetched": 2,
"stoppedWithAnotherPageAvailable": false,
"capturedAt": "2026-07-17T09:30:00.000Z",
"results": [
{
"videoId": "exampleVideo01",
"url": "https://www.youtube.com/watch?v=exampleVideo01",
"title": "Configurarea routerului de acasă",
"channelId": "exampleChannel01",
"channelTitle": "Network Lab",
"publishedAt": "2026-06-12T08:00:00Z",
"page": 1,
"position": 1
}
]
}
Compare like with like: same query spelling, type, region, relevance language, page size, page cap, credential project, and capture window. If one run stops after a quota or network error, mark it incomplete rather than comparing its ten rows with a complete twenty-row run. Store resource IDs as the stable join key; titles and channel display names can change.
Keep rendered regional QA in another dataset
The official API is not a browser screenshot. YouTube’s website can select language and content region using account, browser, domain, and IP signals, while the API exposes documented request parameters. A proxy controls the browser’s network route; it does not modify regionCode or relevanceLanguage in the API program above.
If the research question is “does this public search page load from our Romanian route?”, run a small signed-out browser check and label the output rendered-route-qa. Do not merge its visible order with the API rows. The proxy verification guide explains how to confirm an exit, while the YouTube regional availability guide covers market-specific playback checks.
import { chromium } from 'playwright';
const server = process.env.PROXY_SERVER;
if (!server) throw new Error('PROXY_SERVER is required');
const proxy = { server };
if (process.env.PROXY_USERNAME) proxy.username = process.env.PROXY_USERNAME;
if (process.env.PROXY_PASSWORD) proxy.password = process.env.PROXY_PASSWORD;
let browser;
try {
browser = await chromium.launch({ headless: true, proxy });
const context = await browser.newContext({ locale: 'ro-RO' });
const page = await context.newPage();
const routeResponse = await page.goto('https://api.ipify.org?format=json', {
waitUntil: 'domcontentloaded', timeout: 30_000,
});
if (!routeResponse?.ok()) throw new Error(`Route check HTTP ${routeResponse?.status()}`);
const route = JSON.parse(await page.textContent('body'));
const query = encodeURIComponent('home router setup');
const response = await page.goto(`https://www.youtube.com/results?search_query=${query}`, {
waitUntil: 'domcontentloaded', timeout: 30_000,
});
if (!response?.ok()) throw new Error(`YouTube HTTP ${response?.status()}`);
await page.screenshot({ path: 'youtube-search-ro.png', fullPage: false });
console.log(JSON.stringify({
method: 'rendered-route-qa',
exitIp: route.ip,
locale: 'ro-RO',
finalUrl: page.url(),
title: await page.title(),
screenshot: 'youtube-search-ro.png',
}, null, 2));
} catch (error) {
process.stderr.write(`Regional QA failed: ${error.message}\n`);
process.exitCode = 1;
} finally {
await browser?.close();
}
Install the dependency with npm install playwright and the browser with npx playwright install chromium. The script proves which public IP the browser used and records one page artifact. It intentionally does not contain a changing result-card selector or claim that page order is stable.
Diagnose failures at the method that produced them
An API 400 usually points to request validation, while 403 can include credential, project, permission, or quota details in the error body. A missing nextPageToken is a normal end condition. By contrast, proxy authentication, navigation timeout, consent, and page markup are browser-route problems. Keep those error categories separate.
Before repeating work, confirm the exact method, status, and page count. Do not rotate API keys to evade a quota, and do not broaden browser selectors because a consent page returned no video cards. For route errors, follow the proxy troubleshooting sequence and redact credentials from logs.
Choose the next action from the captured evidence
For recurring catalog research, schedule the bounded API request at the slowest cadence that answers the question and retain the request controls beside every snapshot. For one-off display verification, keep the screenshot and exit proof briefly, then remove them according to the project’s retention rule. Review the YouTube API Services developer policies before turning a prototype into a product.
If rendered QA requires a stable Romanian exit from verification through screenshot capture, compare Mexela’s proxy plans by country coverage and session persistence. That route is for the browser check only; the API run still declares its own region and language parameters.
Frequently asked questions
Does regionCode guarantee Romanian-language results?
No. It asks for videos viewable in the country. relevanceLanguage is a separate relevance hint, and the official documentation says other languages can still appear when highly relevant.
Why not request all available pages?
A declared cap controls quota use and dataset size. If the question truly needs more coverage, raise the cap deliberately and record the resulting call count rather than following tokens without limit.
Can the browser proxy replace regionCode?
No. The proxy affects the rendered browser route. The Data API request remains governed by its documented parameters and credentials.
Should API and browser results be compared position by position?
No. They are different surfaces. Compare each method across equivalent runs, and use browser artifacts only as separately labeled regional QA.

