Twitter API with Node.js - Native Fetch Tutorial
Build a Twitter API integration in Node.js with native fetch, raw HTTP, pagination, bounded retries, and current TweetAPI endpoints.
If you can use fetch, you can call a Twitter data API from Node.js without adding an HTTP library or SDK. This raw HTTP tutorial starts with one small request helper, then uses it for profiles, posts, search, followers, and pagination.
The result is deliberately modest: a script you can read in one sitting and adapt without first learning a framework. Every example uses the current /tw-v2 paths, the X-API-Key header, and camelCase response fields.
Last verified: July 28, 2026.
Disclaimer: TweetAPI is a third-party service. Not affiliated with X Corp.
Choose This Guide If
- You want a native
fetchimplementation with no TweetAPI SDK dependency. - You need direct control over URLs, headers, timeouts, retries, logging, and scheduling.
- You are learning the raw HTTP contract before wrapping it in your own application layer.
If you want typed client methods, built-in error classes, automatic retries, and pagination helpers, use the TweetAPI Python and TypeScript SDK guide instead.
What You Need
- Node.js 22 or 24 LTS for a currently supported production runtime
- A TweetAPI account and API key (100-request trial, no credit card required)
- Basic familiarity with async functions, JSON, and environment variables
The TweetAPI Node.js SDK package supports Node 18+, but Node 18 and 20 are end-of-life. The Node.js release schedule recommends Active or Maintenance LTS versions for production.
Project Setup
Create a project and an ES module:
mkdir twitter-nodejs
cd twitter-nodejs
npm init -y
touch twitter.mjs
Create a .env file:
TWEETAPI_KEY=your_api_key_here
Add .env to .gitignore before committing the project.
Run the script with Node's built-in environment-file support:
node --env-file=.env twitter.mjs
Build a Native Fetch Request Helper
Rather than repeat URL and error handling for every endpoint, start twitter.mjs with one helper and a bounded retry loop:
const API_KEY = process.env.TWEETAPI_KEY;
const BASE_URL = "https://api.tweetapi.com/tw-v2";
const REQUEST_TIMEOUT_MS = 15_000;
if (!API_KEY) {
throw new Error("Missing TWEETAPI_KEY environment variable");
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const isTransientFetchError = (error) =>
error instanceof TypeError ||
(error instanceof DOMException &&
(error.name === "TimeoutError" || error.name === "AbortError"));
async function apiGet(path, params = {}, maxRetries = 3) {
const url = new URL(`${BASE_URL}${path}`);
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== "") {
url.searchParams.set(key, String(value));
}
}
for (let attempt = 0; attempt <= maxRetries; attempt++) {
let response;
try {
response = await fetch(url, {
headers: { "X-API-Key": API_KEY },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
} catch (error) {
if (!isTransientFetchError(error) || attempt === maxRetries) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(
`TweetAPI request failed before a response: ${message}`,
{ cause: error }
);
}
const delayMs = Math.min(1_000 * 2 ** attempt, 30_000);
await sleep(delayMs);
continue;
}
if (response.ok) {
return response.json();
}
const retryable = response.status === 429 || response.status >= 500;
if (!retryable || attempt === maxRetries) {
const body = await response.text();
throw new Error(
`TweetAPI request failed (${response.status}): ${body.slice(0, 300)}`
);
}
const delayMs = Math.min(1_000 * 2 ** attempt, 30_000);
await sleep(delayMs);
}
}
maxRetries = 3 means one initial attempt plus up to three retries, for at most four attempts. Every attempt has a 15-second timeout. The helper retries 429, 5xx, timeout, and transient fetch/network failures with bounded exponential backoff; it does not retry ordinary 4xx responses such as invalid parameters or credentials.
In a larger application, put all requests through a shared queue so concurrent workers respect the plan's per-minute limit. A fixed delay inside one pagination loop does not account for calls made elsewhere in the process.
Fetch a User Profile
The user-by-username endpoint is GET /tw-v2/user/by-username:
async function getUser(username) {
return apiGet("/user/by-username", { username });
}
const userResponse = await getUser("sample_user_96991");
const user = userResponse.data;
console.log(user.id);
console.log(user.name);
console.log(user.followerCount);
console.log(user.bio);
The response is nested under data. Current fields include id, username, name, bio, avatar, banner, followerCount, followingCount, tweetCount, verification fields, and createdAt.
Older examples using /user/by, id_str, or followers_count do not match the current API.
Look Up Multiple Users
Use the bulk username endpoint instead of sending one request per account. It accepts a comma-separated string with up to 50 usernames:
async function getUsers(usernames) {
return apiGet("/user/by-usernames", {
usernames: usernames.join(","),
});
}
const usersResponse = await getUsers(["nodejs", "vercel", "nextjs"]);
for (const profile of usersResponse.data) {
console.log(`@${profile.username}: ${profile.followerCount} followers`);
}
This endpoint is more quota-efficient than an application-level loop for supported batch sizes.
Fetch a User's Posts
The user posts endpoint requires a numeric userId and accepts an optional cursor:
async function getUserTweets(userId, cursor) {
return apiGet("/user/tweets", { userId, cursor });
}
const profile = (await getUser("sample_user_96991")).data;
const tweets = await getUserTweets(profile.id);
for (const post of tweets.data) {
console.log(post.text);
console.log({
likes: post.likeCount,
reposts: post.retweetCount,
replies: post.replyCount,
views: post.viewCount,
});
}
/user/tweets is documented as the user's original-post timeline without replies. If you need replies in the timeline, call the separate tweets-and-replies endpoint.
Search Public Posts
The search endpoint requires a query and a title-case result type:
When a search needs a precise time window, the Tweet ID and timestamp converter can generate date and Snowflake ID boundaries before you build the query.
async function searchTweets(query, type = "Latest", cursor) {
return apiGet("/search", { query, type, cursor });
}
const results = await searchTweets('"node.js" min_faves:10', "Latest");
for (const post of results.data) {
console.log(`@${post.author.username}: ${post.text.slice(0, 120)}`);
}
Supported result types are:
LatestTopPeoplePhotosVideos
The API treats these values as case-sensitive. The search documentation lists supported operators such as from:username, exact phrases, hashtags, date filters, media filters, and engagement thresholds.
Paginate Collection Endpoints
Paginated responses contain a data array and pagination.nextCursor. Pass that cursor to the next request:
async function getTweetPages(userId, maxPages = 3) {
const pages = [];
let cursor;
for (let pageNumber = 0; pageNumber < maxPages; pageNumber++) {
const page = await getUserTweets(userId, cursor);
pages.push(page);
cursor = page.pagination?.nextCursor;
if (!cursor) break;
}
return pages;
}
const pages = await getTweetPages(profile.id, 3);
const allPosts = pages.flatMap((page) => page.data);
console.log(`Fetched ${pages.length} page(s) and ${allPosts.length} posts`);
Always set a page or record ceiling. An unbounded cursor loop can exhaust quota or collect more data than the application needs.
Fetch Followers
The followers endpoint also accepts userId and cursor and returns user objects:
async function getFollowers(userId, cursor) {
return apiGet("/user/followers", { userId, cursor });
}
const followers = await getFollowers(profile.id);
for (const follower of followers.data) {
console.log(`@${follower.username}: ${follower.followerCount} followers`);
}
console.log("Next cursor:", followers.pagination?.nextCursor);
Because each item is a user object, one page can support basic audience summaries without a follow-up profile request for every returned account.
Build a Small Account Report
Now we can turn those helpers into something useful. This small report combines one profile request with one page of original posts:
async function analyzeAccount(username) {
const user = (await getUser(username)).data;
const timeline = await getUserTweets(user.id);
const posts = timeline.data;
const totals = posts.reduce(
(summary, post) => ({
likes: summary.likes + (post.likeCount ?? 0),
reposts: summary.reposts + (post.retweetCount ?? 0),
replies: summary.replies + (post.replyCount ?? 0),
views: summary.views + (post.viewCount ?? 0),
}),
{ likes: 0, reposts: 0, replies: 0, views: 0 }
);
const divisor = posts.length || 1;
return {
account: {
id: user.id,
username: user.username,
followers: user.followerCount,
following: user.followingCount,
totalPosts: user.tweetCount,
createdAt: user.createdAt,
},
sample: {
posts: posts.length,
averageLikes: Math.round(totals.likes / divisor),
averageReposts: Math.round(totals.reposts / divisor),
averageReplies: Math.round(totals.replies / divisor),
averageViews: Math.round(totals.views / divisor),
},
nextCursor: timeline.pagination?.nextCursor ?? null,
};
}
const username = process.argv[2] ?? "sample_user_96991";
console.log(JSON.stringify(await analyzeAccount(username), null, 2));
Run it with:
node --env-file=.env twitter.mjs sample_user_96991
The base report makes two ordinary public-data calls: one profile request and one timeline page. Retries, additional pages, or extra endpoints use more quota. Specialized authentication and messaging endpoints can also have different quota weights, so do not extrapolate all operations from this example.
Current TweetAPI Limits
| Plan | Monthly quota | Per-minute limit |
|---|---|---|
| Free | 100 one-time requests | 10 |
| Pro ($17/month) | 100,000 | 60 |
| Ultra ($57/month) | 500,000 | 120 |
| Mega ($197/month) | 2,000,000 | 180 |
Choose a plan from measured request volume and peak concurrency, not from an account-count rule of thumb. See current pricing before purchase.
Production Checklist
- Keep TweetAPI keys in a server-side secret store.
- Never expose an API key in browser JavaScript.
- Set a request timeout appropriate to the endpoint and caller; this example uses 15 seconds per attempt.
- Put concurrent requests through a shared rate-aware queue.
- Bound retries, pagination depth, and total records.
- Cache responses only as long as your use case and applicable policies permit.
- Log status, endpoint, latency, and attempt count without logging credentials.
- Validate response fields before inserting data into a database.
- Design a deletion and retention process for stored social data.
Raw HTTP vs the TweetAPI SDK
Raw fetch is useful when you want no runtime dependency and full control over retry, logging, and scheduling. The TweetAPI-maintained SDKs add typed methods, standardized errors, automatic transient retries, and pagination helpers.
npm install tweetapi-node
import TweetAPI from "tweetapi-node";
const client = new TweetAPI({ apiKey: process.env.TWEETAPI_KEY });
const user = await client.user.getByUsername({
username: "sample_user_96991",
});
console.log(user.data.followerCount);
Read the Python and Node.js SDK guide for retry and pagination examples.
FAQ
Do I need an official X developer account?
Not for the TweetAPI public-data flow shown here. You need a TweetAPI account and API key. Use the official X API when your application needs official OAuth, complete archive search, filtered stream, or a direct X relationship.
Does this work with Express, Next.js, and serverless functions?
Yes, if the runtime provides a supported Node.js version and server-side environment variables. Reuse one request queue where possible and account for serverless concurrency when enforcing per-minute limits.
Can I use TypeScript?
Yes. You can type the raw JSON yourself or use tweetapi-node, which ships request and response types.
Why recommend Node 22+ when the SDK supports Node 18?
The package's compatibility floor and Node's security-support lifecycle are different. Node 18 and 20 are end-of-life as of July 2026; Node 22 and 24 are maintained LTS releases.
How should I handle HTTP 429?
Pause requests, use bounded exponential backoff, and reduce concurrency. Do not assume every response includes a Retry-After header; use a conservative fallback or the SDK's rate-limit handling.
Can I post, like, or follow with TweetAPI?
TweetAPI documents supported interaction endpoints. These are POST requests and require an authToken plus endpoint-specific fields; some workflows require additional parameters. Start with the favorite-post documentation and follow the exact page for the action you need.
Where to Take It Next
Run the account report against a test username, inspect the raw response, and measure how many pages the real workflow needs. From there, the full documentation shows the other endpoints, while the SDK guide is the shorter path if you would rather not maintain retry and pagination code yourself.
For complete Python projects, use the same API to export Twitter followers to a resumable CSV or build a Twitter brand monitor with SQLite and Telegram alerts.
Before choosing a plan, compare your measured call volume with current pricing. The pricing guide explains why TweetAPI request quotas and X resource charges are different units.
Use the 100-request trial to run the script. No credit card is required.