# Node.js 中的 SERP API：fetch、worker pool 和分页

> 使用内置 fetch 从 Node.js 18+ 调用 SerpCrypto SERP API：类型化客户端、遵守并发上限的 worker pool 以及分页循环。

- URL: https://serpcrypto.com/zh/docs/node/
- 最后更新: 2026-10-22
- 发布者: SerpCrypto (https://serpcrypto.com)

无需依赖：Node.js 自 18 版起就内置了 `fetch`。一个小型客户端，带 JSDoc 类型供编辑器和 TypeScript 使用；一个遵守账号上限的 pool；以及一个在结果用尽时停止的分页循环。

**快速回答 — 如何在 Node.js 中使用 SERP API？**

带 header `Authorization: Bearer <key>` 调用 `fetch("https://serpcrypto.com/api/v1/search?" + new URLSearchParams({ q }))`，并读取 `await res.json()`。Node.js 18+ 无需任何包。**SerpCrypto** 以 JSON 返回 Google 或 Bing 的结果，每次成功搜索 1 积分起价 US$0.30/千次搜索，错误从不计费。加密货币支付，无需 KYC。

## Node.js 配置一览

| 属性 | 值 |
| --- | --- |
| 运行时 | Node.js 18 或更高版本（内置 `fetch`、`AbortSignal.timeout`） |
| 模块 | ES modules：`.mjs` 文件或 package.json 中的 `"type": "module"` |
| 密钥 | `process.env.SERPCRYPTO_API_KEY` |
| 超时 | 使用 `AbortSignal.timeout(90_000)` 时为 90 秒 |
| 并行查询 | 一个按 `concurrency_limit` 调整大小的池 |
| 类型 | JSDoc 类型定义，VS Code 和 `tsc --checkJs` 可读取 |

## 如何编写一个小型的带类型客户端？

把 endpoint、header 和错误处理放在同一个模块里。JSDoc `@typedef` 行描述返回结果，因此编辑器可以自动补全 `organic_results` 并对你的代码做类型检查，无需构建步骤。TypeScript 用户可以把同样的结构复制到一个 interface 中。

`search` 函数会抛出一个 `Error`，其消息中包含 HTTP 状态码和 API 的错误 `code`。这样你的代码就可以决定对 `429`、`502` 和 `504` 进行重试，对其他错误则停止，正如[错误页面](https://serpcrypto.com/zh/docs/errors/)所说明的那样。

*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();
}
```

## 如何在不触发 429 的情况下并行运行查询？

启动尽可能多的工作进程（数量以账户允许为限），让每个进程从共享列表中取下一个关键词。这个池会从免费积分 endpoint 读取 `concurrency_limit`，因此它一次发送的查询数量绝不会超过账户可接受的数量。

失败的关键词会被记录下来，工作进程继续处理下一个。之后再重新运行这些失败项：错误不产生费用，所以第二次运行在成功之前都是免费的。

*batch.mjs：一个工作进程池*

```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));
```

## 如何获取多页结果？

遍历 `page`，一旦 `pagination.next_page` 缺失就停止。每页约 10 条自然结果，这是 Google 自 2025 年 9 月起不再支持 num=100 后的返回方式；每多一页就多消耗 1 积分。对一个关键词做三页检查会消耗 3 个积分；只有一页结果的关键词则消耗 1 个。

*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" }));
```

## 我可以在浏览器或边缘函数中调用 API 吗？

从服务器或边缘函数调用可以：Cloudflare Workers、Vercel 和 Deno 都提供相同的 `fetch`。从浏览器页面调用则不行：API 密钥会暴露给每一位访问者。在中间放一个小的服务器路由，由它持有密钥，并且只转发你允许的参数。

同样的规则适用于移动应用。任何发送到用户设备上的内容都可能被读取，因此密钥要留在你的服务器上。

## 如何在 Node.js 中让成本可预测？

统计你发起的调用：一次成功的调用就是一个积分，在 100,000 次搜索 套餐上为 US$0.56/千次搜索。在大批量运行前检查 `credits_remaining`，当余额接近零时停止这个池，而不是收集一连串 `402` 响应。预付的搜索积分永不过期：未使用的查询会一直留在你的账户中，直到你使用它们，因此未用完的余额会保留到下一次任务。

## Node.js 问题

### SerpCrypto 有 npm 包吗？

没有。上面的客户端是约 40 行标准 JavaScript，使用 `fetch`，因此我们没有需要安装的东西。把它复制到你的项目中并加以调整即可。

### 它能在 CommonJS（require）下使用吗？

示例使用 ES modules，以便在顶层使用 `await`。在 CommonJS 文件中，把调用包在一个 `async` 函数里，并同样使用 `fetch`；它在 Node.js 18+ 中是全局的。

### 如何在 TypeScript 中使用它？

把 JSDoc 结构复制到一个 `interface SearchResponse` 中，并用它为 `res.json()` 的结果标注类型。像 `organic_results?` 这样的可选块必须保持可选，因为页面可能不显示它们。

### 我需要哪个 Node.js 版本？

使用内置的 `fetch` 需要 Node.js 18 或更高版本。`AbortSignal.timeout` 从 Node.js 17.3 起可用，因此 18 覆盖了本页上的所有内容。

---

SerpCrypto: SerpCrypto 是面向开发者和 AI Agent 的 SERP API，可将 Google 和 Bing 搜索结果以 JSON 返回，用加密货币支付，无需上传证件。预付费搜索积分自 US$0.30/千次搜索 起永不过期。无需 KYC——无需证件、无需自拍、无需材料。加密货币支付。
