Skip to content

NODE.JS

Twitter API Node.js client

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.

twitterapis.mjs
1const BASE = "https://api.twitterapis.com";
2const KEY = process.env.TWITTERAPIS_KEY;
3
4async function get(path, params = {}) {
5 const url = new URL(`/twitter/${path}`, BASE);
6 for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
7
8 const res = await fetch(url, {
9 headers: { Authorization: `Bearer ${KEY}` },
10 });
11 if (!res.ok) {
12 throw new Error(`${res.status} ${await res.text()}`);
13 }
14 return res.json();
15}
16
17// Resolve a handle to the permanent numeric user ID
18const { user } = await get("user/info", { username: "naval" });
19console.log(user.id, user.followers_count);
20
21// Search posts
22const page = await get("tweet/advanced_search", {
23 query: "from:naval min_faves:500",
24});
25for (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.

client.ts
1type User = {
2 id: string;
3 username: string;
4 name: string;
5 followers_count: number;
6 verified: boolean;
7};
8
9type Page<T> = { next_cursor?: string } & T;
10
11async function get<T>(path: string, params: Record<string, string> = {}): Promise<T> {
12 const url = new URL(`/twitter/${path}`, "https://api.twitterapis.com");
13 for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
14
15 const res = await fetch(url, {
16 headers: { Authorization: `Bearer ${process.env.TWITTERAPIS_KEY}` },
17 signal: AbortSignal.timeout(30_000),
18 });
19 if (!res.ok) throw new Error(`twitterapis: ${res.status}`);
20 return res.json() as Promise<T>;
21}
22
23const { 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.

Pagination
1async function* paginate(path, params = {}) {
2 let cursor;
3 do {
4 const page = await get(path, cursor ? { ...params, cursor } : params);
5 yield page;
6 cursor = page.next_cursor;
7 } while (cursor);
8}
9
10let total = 0;
11for await (const page of paginate("user/followers", { username: "naval" })) {
12 total += page.followers.length;
13}
14console.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.

Retries
1async function getWithRetry(path, params, attempts = 3) {
2 for (let i = 0; i < attempts; i++) {
3 try {
4 return await get(path, params);
5 } catch (err) {
6 // A 4xx will not become a 2xx on the second try, and every retry is
7 // another billed call, so only server errors are worth repeating.
8 const status = Number(String(err.message).match(/\d{3}/)?.[0]);
9 if (status && status < 500) throw err;
10 if (i === attempts - 1) throw err;
11 await new Promise((r) => setTimeout(r, 500 * 2 ** i));
12 }
13 }
14}

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. 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.