NODE.JS
Twitter API Node.js client
How do I call the Twitter API from Node.js?
The Node.js client for TwitterAPIs is a single function over the fetch built into the runtime, so the dependency count is zero. Node 18 and later ship fetch, URL and AbortSignal in the runtime, which covers URL building, authentication and timeouts without adding anything to your package file. One helper function then covers every read endpoint the API serves.
No dependency. fetch is built in from Node 18.
const BASE = "https://api.twitterapis.com";
const KEY = process.env.TWITTERAPIS_KEY;
async function get(path, params = {}) {
const url = new URL(`/twitter/${path}`, BASE);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
throw new Error(`${res.status} ${await res.text()}`);
}
return res.json();
}
// Resolve a handle to the permanent numeric user ID
const { user } = await get("user/info", { username: "naval" });
console.log(user.id, user.followers_count);
// Search posts
const page = await get("tweet/advanced_search", {
query: "from:naval min_faves:500",
});
for (const tweet of page.tweets) console.log(tweet.id, tweet.text);The same client in TypeScript
Adding a generic to the helper gives you a typed return without any extra machinery. Hand-written types are practical when you call two or three endpoints, and the public OpenAPI specification will generate them for you when you call many more.
type User = {
id: string;
username: string;
name: string;
followers_count: number;
verified: boolean;
};
type Page<T> = { next_cursor?: string } & T;
async function get<T>(path: string, params: Record<string, string> = {}): Promise<T> {
const url = new URL(`/twitter/${path}`, "https://api.twitterapis.com");
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.TWITTERAPIS_KEY}` },
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error(`twitterapis: ${res.status}`);
return res.json() as Promise<T>;
}
const { user } = await get<{ user: User }>("user/info", { username: "naval" });Pagination with an async generator
Cursor handling belongs in one place. An async generator hides it entirely, so the calling code is a for await loop that reads like the list is already in memory.
How many times that generator yields is worth knowing before you await it, because billing is per call rather than per record. Across 396,817 successful tweet-returning read calls on our own billing logs, 13 to 17 August 2026, a timeline read returned 18.78 records on average and a search read returned 7.62, with 29.5% of search calls returning nothing at all and still counting. Size a job from the figure for the endpoint you are actually calling, not from the page size you requested.
async function* paginate(path, collection, params = {}, maxCalls = 500) {
let cursor;
let calls = 0;
const seen = new Set();
do {
const page = await get(path, cursor ? { ...params, cursor } : params);
if (++calls >= maxCalls)
throw new Error(`hit the ${maxCalls}-call cap, resume from ${cursor}`);
const rows = page[collection] ?? [];
// BOTH terminators: search nulls the cursor on a page that is still full,
// follower-graph endpoints never null it. Either guard alone loops.
if (rows.length === 0) return;
yield rows;
cursor = page.next_cursor;
if (cursor && seen.has(cursor)) // a stuck cursor AND any cycle
throw new Error(`cursor stopped advancing at ${cursor}`);
if (cursor) seen.add(cursor);
} while (cursor);
}
let total = 0;
for await (const rows of paginate("user/followers", "users", { username: "naval" })) {
total += rows.length;
}
console.log(total);Retries, without paying for them twice
Every attempt that reaches the API is a billed call. Backoff on server errors, fail fast on client errors, and a bad key costs you one call rather than three.
async function getWithRetry(path, params, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
return await get(path, params);
} catch (err) {
// A 4xx will not become a 2xx on the second try, and every retry is
// another billed call, so only server errors are worth repeating.
const status = Number(String(err.message).match(/\d{3}/)?.[0]);
if (status && status < 500) throw err;
if (i === attempts - 1) throw err;
await new Promise((r) => setTimeout(r, 500 * 2 ** i));
}
}
}Where to go next
The same client shape in Python and Go, or all six languages on the language clients page. The REST API reference covers the error contract these helpers branch on, and the MCP server is the published npm package for agent workflows.
Frequently Asked Questions
No. Node 18 and later ship fetch, URL and AbortSignal in the runtime, which is everything the client on this page uses. That means zero dependencies, nothing to keep current, and no supply-chain surface added to your project for the sake of saving a dozen lines you can read in full.
Yes, and the typed version on this page is the same function with generics added. Because responses are ordinary JSON with a named payload key, hand-written types for the two or three endpoints you use are usually enough. For a wide integration, generate types from the public OpenAPI 3.1 specification instead of writing them.
Pass signal: AbortSignal.timeout(ms) in the fetch options. Without it, a request that never completes will hang the job rather than fail it, and a hung scheduled job is harder to notice than a failed one. Thirty seconds is a reasonable default for a read call.
Not directly, and not because of a missing package. The API answers a cross-origin preflight without permissive CORS headers, so a browser will block the call, and putting a real API key in client-side JavaScript would publish it to anybody who opens the network tab. Call it from a server route or an edge function and let the browser talk to your own origin.
Use an async generator that reads next_cursor from each response and passes it back on the following call. The consumer then writes a for await loop and never handles cursors. Each iteration is one billed call, so add a page cap while you are developing, otherwise a test run against a large account walks the whole follower list.
$0.0008 for a standard read, the same as from any other language, billed per call with no plan minimum. New accounts start with $0.50 of credit, about 625 read calls, so you can size a job against real data first. Retry logic is the thing to watch: a naive loop that retries a 401 three times bills you three times for a request that was never going to succeed, which is why the retry helper here rejects client errors immediately.