Migration guide · Node.js

Move Node.js code off the Google Custom Search JSON API

Node.js code calls the Custom Search JSON API with fetch or with Google's googleapis package. Either way the move to Searlo's drop-in is the host and the key: one changed URL with fetch, one rootUrl option with googleapis. Response objects and errors keep their shape, ahead of Google's January 1, 2027 cut-off.

The change

With fetch, the URL and the key. With googleapis, pass rootUrl when you create the client: it keeps Google's paths (/customsearch/v1 and /customsearch/v1/siterestrict), which Searlo serves.

- const API_URL = "https://www.googleapis.com/customsearch/v1";
- const API_KEY = process.env.GOOGLE_API_KEY;
+ const API_URL = "https://api.searlo.tech/customsearch/v1";
+ const API_KEY = process.env.SEARLO_API_KEY;
- const customsearch = google.customsearch({ version: "v1", auth: GOOGLE_API_KEY });
+ const customsearch = google.customsearch({ version: "v1", auth: SEARLO_API_KEY,
+                                            rootUrl: "https://api.searlo.tech/" });

Switch in four steps

  1. 01Create a Searlo API keyIn the Searlo dashboard. Store it as SEARLO_API_KEY.
  2. 02Change the hostfetch: replace https://www.googleapis.com/customsearch/v1 with https://api.searlo.tech/customsearch/v1. googleapis: add rootUrl: "https://api.searlo.tech/" to google.customsearch({ version: "v1", … }).
  3. 03Swap the keyPass the Searlo key as key (fetch) or auth (googleapis). Keep cx: it is accepted and has no effect.
  4. 04Restrict to your sites, if your engine didAdd siteSearch: "example.com" to the call. Searlo searches the whole web and cannot read the site list behind a cx.

With fetch

A complete ES module for Node 18 or later: search, read items, follow queries.nextPage to the second page.

search.mjs

const API_URL = "https://api.searlo.tech/customsearch/v1"; // was https://www.googleapis.com/customsearch/v1
const API_KEY = process.env.SEARLO_API_KEY;                 // was your Google API key

async function search(query, start = 1, num = 10) {
  const params = new URLSearchParams({ key: API_KEY, cx: "YOUR_CX", q: query, num: String(num), start: String(start) });
  const res = await fetch(`${API_URL}?${params}`, { signal: AbortSignal.timeout(30_000) });
  const data = await res.json();
  if (!res.ok) {
    // Google's error envelope, unchanged
    throw new Error(`${data.error.code} ${data.error.errors[0].reason}: ${data.error.message}`);
  }
  return data;
}

// The first two pages, following queries.nextPage as you did on Google.
let data = await search("lectures");
for (const item of data.items ?? []) console.log(item.title, item.link);

const next = data.queries.nextPage?.[0];
if (next) {
  data = await search("lectures", next.startIndex);
  for (const item of data.items ?? []) console.log(item.title, item.link);
}

With googleapis

rootUrl on google.customsearch() applies to every method of that client, cse.list and cse.siterestrict.list alike. A trailing slash is optional.

search-client.mjs

import { google } from "googleapis";

const customsearch = google.customsearch({
  version: "v1",
  auth: process.env.SEARLO_API_KEY,  // was your Google API key
  rootUrl: "https://api.searlo.tech/", // the one new option
});

try {
  const { data } = await customsearch.cse.list({ cx: "YOUR_CX", q: "lectures", num: 10 });
  for (const item of data.items ?? []) console.log(item.title, item.link);
  console.log(data.searchInformation.totalResults, data.queries.nextPage?.[0]?.startIndex);

  // The siterestrict resource works too; restrict with siteSearch, since cx has no effect.
  const site = await customsearch.cse.siterestrict.list({ q: "lectures", siteSearch: "ocw.mit.edu" });
  for (const item of site.data.items ?? []) console.log(item.title, item.link);
} catch (err) {
  // Errors keep Google's envelope: 400 keyInvalid, 403 dailyLimitExceeded, 429 rateLimitExceeded...
  const error = err.response?.data?.error;
  console.error(err.status, error?.errors?.[0]?.reason, error?.message);
  process.exitCode = 1;
}

Notes

Each call is 1 credit, whatever num (1 to 10) asks for. A call that returns an error is not charged.

googleapis throws on any error status; err.status is the HTTP status and err.response.data.error is Google's envelope, with errors[0].reason set to keyInvalid, dailyLimitExceeded (out of credits, clears when you top up), rateLimitExceeded and so on.

Call the API from your server. Browsers on other sites are blocked by CORS, and that also keeps the key out of client code.

We ran both modules on 2026-10-25 with Node.js 24 and googleapis 170.1.0.

FAQ

Questions

Does the googleapis package accept a different host?

Yes. Every generated API reads rootUrl from its options, so google.customsearch({ version: "v1", auth: key, rootUrl: "https://api.searlo.tech/" }) sends every call to Searlo with Google's paths.

What about @googleapis/customsearch?

It is the same generated client published on its own, and takes the same option: customsearch({ version: "v1", auth: key, rootUrl: "https://api.searlo.tech/" }). We ran it with version 12.0.1.

And LangChain.js GoogleCustomSearch?

Its URL is fixed in its code, so it cannot be pointed elsewhere. The LangChain guide has a drop-in tool with the same name and output.

Run your code on Searlo

Create a key, change the host, run your tests.