# SERP API trong Node.js: fetch, một worker pool và phân trang

> Gọi SERP API của SerpCrypto từ Node.js 18+ với fetch có sẵn: một client có kiểu, một worker pool trong giới hạn concurrency của bạn và một vòng lặp phân trang.

- URL: https://serpcrypto.com/vi/docs/node/
- Cập nhật lần cuối: 2026-10-22
- Đơn vị phát hành: SerpCrypto (https://serpcrypto.com)

Không cần dependency: Node.js có sẵn `fetch` từ phiên bản 18. Một client nhỏ với kiểu JSDoc cho editor và TypeScript, một pool tôn trọng giới hạn của tài khoản bạn, và một vòng lặp phân trang dừng khi hết kết quả.

**Trả lời nhanh — Làm sao dùng SERP API trong Node.js?**

Gọi `fetch("https://serpcrypto.com/api/v1/search?" + new URLSearchParams({ q }))` với header `Authorization: Bearer <key>` và đọc `await res.json()`. Node.js 18+ không cần package nào. **SerpCrypto** trả về kết quả Google hoặc Bing dưới dạng JSON với 1 credit mỗi lần tìm kiếm thành công, từ 0,30 US$/1K lượt tìm kiếm, và không bao giờ tính phí lỗi. Thanh toán bằng crypto, không cần KYC.

## Thiết lập Node.js trong nháy mắt

| Thuộc tính | Giá trị |
| --- | --- |
| Runtime | Node.js 18 trở lên (có sẵn `fetch`, `AbortSignal.timeout`) |
| Modules | ES modules: tệp `.mjs` hoặc `"type": "module"` trong package.json |
| Khóa | `process.env.SERPCRYPTO_API_KEY` |
| Timeout | 90 giây với `AbortSignal.timeout(90_000)` |
| Tìm kiếm song song | Một pool có kích thước bằng `concurrency_limit` |
| Kiểu dữ liệu | JSDoc typedefs, được VS Code và `tsc --checkJs` đọc |

## Làm thế nào để viết một client nhỏ có kiểu dữ liệu?

Đặt endpoint, header và xử lý lỗi trong một module. Các dòng JSDoc `@typedef` mô tả câu trả lời, nên trình soạn thảo tự động hoàn thành `organic_results` và kiểm tra kiểu cho mã của bạn mà không cần bước build. Người dùng TypeScript có thể sao chép cùng các cấu trúc đó vào một interface.

Hàm `search` ném một `Error` với HTTP status và `code` lỗi của API trong thông điệp của nó. Mã của bạn sau đó có thể quyết định thử lại `429`, `502` và `504` và dừng lại với các trường hợp khác, như [trang lỗi](https://serpcrypto.com/vi/docs/errors/) giải thích.

*serp-client.mjs*

```javascript
// serp-client.mjs — Node.js 18+, no dependency
const API_URL = "https://serpcrypto.com/api/v1/search";
const CREDITS_URL = "https://serpcrypto.com/api/v1/credits";
const headers = { Authorization: `Bearer ${process.env.SERPCRYPTO_API_KEY}` };

/**
 * @typedef {Object} OrganicResult
 * @property {number} position
 * @property {string} title
 * @property {string} link
 * @property {string} [snippet]
 * @property {string} [date]
 */

/**
 * @typedef {Object} SearchResponse
 * @property {Object} search_metadata
 * @property {string} search_metadata.id
 * @property {string} search_metadata.status
 * @property {number} search_metadata.credits_used
 * @property {OrganicResult[]} [organic_results]
 * @property {Object} [pagination]
 * @property {number} pagination.current
 * @property {number} [pagination.next_page]
 */

/**
 * Run one search (1 credit when it succeeds).
 * @param {string} q
 * @param {Record<string, string | number>} [params]
 * @returns {Promise<SearchResponse>}
 */
export async function search(q, params = {}) {
  const query = new URLSearchParams({ q, ...Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)])) });
  const res = await fetch(`${API_URL}?${query}`, { headers, signal: AbortSignal.timeout(90_000) });
  if (!res.ok) {
    const body = await res.json().catch(() => ({}));
    const err = new Error(`${res.status} ${body.error?.code ?? "error"}: ${body.error?.message ?? ""}`);
    err.status = res.status;
    throw err;
  }
  return res.json();
}

/** Free: credits left, used today and the concurrency limit. */
export async function credits() {
  const res = await fetch(CREDITS_URL, { headers });
  if (!res.ok) throw new Error(`credits: HTTP ${res.status}`);
  return res.json();
}
```

## Làm thế nào để chạy tìm kiếm song song mà không bị 429?

Khởi động số worker tối đa mà tài khoản của bạn cho phép, và để mỗi worker lấy từ khóa tiếp theo từ một danh sách chung. Pool đọc `concurrency_limit` từ endpoint credit miễn phí, nên nó không bao giờ gửi nhiều tìm kiếm cùng lúc hơn mức tài khoản chấp nhận.

Một từ khóa thất bại được ghi lại và worker chuyển sang từ khóa tiếp theo. Chạy lại các từ khóa thất bại sau đó: lỗi không tốn gì, nên lượt chạy thứ hai miễn phí cho đến khi thành công.

*batch.mjs: một pool worker*

```javascript
// batch.mjs — parallel searches, never more than the account's limit at once
import { search, credits } from "./serp-client.mjs";

const keywords = ["password manager", "note taking app", "time tracking software", "invoice software"];
const { concurrency_limit: limit } = await credits();

let next = 0;
const results = new Map();

async function worker() {
  while (next < keywords.length) {
    const kw = keywords[next++];
    try {
      const data = await search(kw, { gl: "us", hl: "en" });
      results.set(kw, data.organic_results?.[0]?.link ?? null);
    } catch (err) {
      results.set(kw, `failed: ${err.message}`);
    }
  }
}

await Promise.all(Array.from({ length: Math.min(limit, keywords.length) }, worker));
console.table(Object.fromEntries(results));
```

## Làm thế nào để lấy nhiều trang kết quả?

Lặp qua `page` và dừng ngay khi `pagination.next_page` không còn. Khoảng 10 kết quả organic mỗi trang, đúng như Google đã trả về kể từ khi ngừng áp dụng num=100 vào tháng 9 năm 2025; mỗi trang thêm là thêm một credit. Kiểm tra ba trang cho một từ khóa tốn ba credit; một từ khóa chỉ có một trang kết quả tốn một credit.

*pages.mjs*

```javascript
// pages.mjs — up to three pages, stop when results end
import { search } from "./serp-client.mjs";

async function topResults(q, pages = 3, params = {}) {
  const rows = [];
  for (let page = 1; page <= pages; page++) {
    const data = await search(q, { ...params, page });
    for (const r of data.organic_results ?? []) rows.push({ page, position: r.position, link: r.link });
    if (!data.pagination?.next_page) break; // nothing more: save the credits
  }
  return rows;
}

console.log(await topResults("headless cms", 3, { gl: "de", hl: "en" }));
```

## Tôi có thể gọi API từ trình duyệt hoặc edge function không?

Từ một server hoặc edge function, có: Cloudflare Workers, Vercel và Deno đều cung cấp cùng `fetch`. Từ một trang trình duyệt, không: khóa sẽ hiển thị với mọi người truy cập. Hãy đặt một route server nhỏ ở giữa để giữ khóa và chỉ chuyển tiếp các tham số bạn cho phép.

Quy tắc tương tự áp dụng cho ứng dụng di động. Bất cứ thứ gì gửi đến thiết bị của người dùng đều có thể bị đọc, nên khóa phải nằm trên server của bạn.

## Làm thế nào để giữ chi phí dự đoán được trong Node.js?

Đếm số lần gọi bạn thực hiện: một lần gọi thành công là một credit, với 0,56 US$/1K lượt tìm kiếm trên pack 100.000 tìm kiếm. Kiểm tra `credits_remaining` trước một batch lớn và dừng pool khi gần về 0 thay vì thu thập một loạt câu trả lời `402`. Credit tìm kiếm trả trước không bao giờ hết hạn: các lượt tìm kiếm chưa dùng vẫn nằm trong tài khoản của bạn cho đến khi bạn dùng, nên phần dư chưa dùng được giữ lại cho công việc tiếp theo.

## Câu hỏi về Node.js

### Có package npm cho SerpCrypto không?

Không. Client ở trên chỉ khoảng 40 dòng JavaScript tiêu chuẩn dùng `fetch`, nên không có gì của chúng tôi để cài đặt. Sao chép nó vào dự án của bạn và điều chỉnh.

### Cái này có hoạt động với CommonJS (require) không?

Các ví dụ dùng ES modules để `await` ở cấp cao nhất. Trong một tệp CommonJS, hãy bọc các lời gọi trong một hàm `async` và dùng `fetch` theo cùng cách; nó là global trong Node.js 18+.

### Làm thế nào để dùng với TypeScript?

Sao chép các cấu trúc JSDoc vào một `interface SearchResponse` và gán kiểu cho kết quả của `res.json()` với nó. Các khối tùy chọn như `organic_results?` phải vẫn là tùy chọn, vì một trang có thể không hiển thị chúng.

### Tôi cần phiên bản Node.js nào?

Node.js 18 trở lên cho `fetch` tích hợp sẵn. `AbortSignal.timeout` có từ Node.js 17.3, nên 18 bao phủ mọi thứ trên trang này.

---

SerpCrypto: SerpCrypto là SERP API cho lập trình viên và AI agent, trả về kết quả Google và Bing dưới dạng JSON, thanh toán bằng crypto mà không cần tải lên giấy tờ tùy thân. Credit tìm kiếm trả trước từ 0,30 US$/1K lượt tìm kiếm không bao giờ hết hạn. Không cần KYC — không ID, không selfie, không giấy tờ. Thanh toán bằng crypto.
