|
| 1 | +<p align="center"><a href="https://apify.com/DevilScrapes/github-user-scraper"><img src="assets/banner.svg" alt="GitHub User Scraper — Profiles, Repos & Orgs — JavaScript example" width="100%"></a></p> |
| 2 | + |
| 3 | +# GitHub User Scraper for JavaScript and Node.js: account and org lookups for your backend |
| 4 | + |
| 5 | +<p align="center"> |
| 6 | +<a href="https://apify.com/DevilScrapes/github-user-scraper"><img alt="Run on Apify" src="https://img.shields.io/badge/Run%20on-Apify-DC2626?logo=apify&logoColor=white"></a> |
| 7 | +<img alt="JavaScript" src="https://img.shields.io/badge/Node.js-20%2B-339933?logo=nodedotjs&logoColor=white"> |
| 8 | +<a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-0A0606"></a> |
| 9 | +<a href="https://github.com/devilscrapes/github-user-scraper-javascript/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/devilscrapes/github-user-scraper-javascript/actions/workflows/ci.yml/badge.svg"></a> |
| 10 | +</p> |
| 11 | + |
| 12 | +Use this repository to call the **[GitHub User Scraper](https://apify.com/DevilScrapes/github-user-scraper)** from Node.js, using a single ES module and the `apify-client` package that Apify maintains. Send a list of logins or profile links and await an array of objects, one per account, carrying follower and repository totals, profile links, timestamps and, optionally, a nested array of recently pushed public repositories. Apify runs the lookups and deals with retries, so your Express handler, worker or scheduled function never has to manage GitHub's request budget itself. Rows end up in `results.json` and `results.csv`, or stay in memory when you import the call into your own code. |
| 13 | + |
| 14 | +> **Actor:** [GitHub User Scraper — Profiles, Repos & Orgs](https://apify.com/DevilScrapes/github-user-scraper) · **Publisher:** [Devil Scrapes](https://apify.com/DevilScrapes) · **Also in Python:** [github-user-scraper-python](https://github.com/devilscrapes/github-user-scraper-python) |
| 15 | +
|
| 16 | +## 🔥 What you get |
| 17 | + |
| 18 | +- **People and organizations in one schema**: both kinds of account come back with identical keys, and `type` says `User` or `Organization` so you can split them later. |
| 19 | +- **Paste logins or links**: entries in `usernames` may be bare slugs or full profile URLs; the host part is removed before lookup. |
| 20 | +- **Counts as integers**: `followers`, `following`, `public_repos` and `public_gists` are numbers, ready to sort, sum or chart. |
| 21 | +- **Recent repositories on request**: set `includeRepos` and each row gains a `repos` list of the account's most recently updated public repositories, each with name, description, language, stars, forks, fork and archived flags and last push time. `maxReposPerUser` caps it at up to 100. |
| 22 | +- **Bring a token if you need volume**: without one, GitHub allows 60 requests an hour; an optional personal access token lifts that to 5,000. |
| 23 | +- **Back-off built in**: on throttling or server errors the Actor waits and retries, and it reports a clear status message instead of finishing with a silently empty dataset. |
| 24 | +- **Only what is public**: `email` is filled only when the account owner chose to publish one, and private repositories or memberships are never touched. |
| 25 | + |
| 26 | +## 🎯 Use cases |
| 27 | + |
| 28 | +### Show live GitHub stats in a product page |
| 29 | + |
| 30 | +Refresh the numbers for your own organization on a timer, cache the row and render follower and repository totals in a footer or an about page. The page stays current without the browser calling GitHub. |
| 31 | + |
| 32 | +### Enrich sign-ups that include a GitHub handle |
| 33 | + |
| 34 | +When a user connects or types a GitHub handle, queue a run for it and store `public_repos`, `followers` and `created_at` next to the account. A support or sales tool can then tell a brand-new account from a long-standing maintainer. |
| 35 | + |
| 36 | +### Post a weekly webhook digest |
| 37 | + |
| 38 | +Run the Actor on an Apify schedule, diff the rows against last week's copy and send the deltas to Slack, Discord or any webhook endpoint. DevRel teams get follower and repository changes for themselves and peers without opening a browser. |
| 39 | + |
| 40 | +### Feed an internal developer directory |
| 41 | + |
| 42 | +For an employee directory or an open-source program office, keep each team account's `blog`, `location` and recent `repos` in your database and serve them through your own API, keyed by `login`. |
| 43 | + |
| 44 | +## ⚡ Quick start |
| 45 | + |
| 46 | +You need an Apify account and an API token from [Console → Settings → Integrations](https://console.apify.com/settings/integrations). Apify's free plan includes monthly usage credit; this Actor's event prices are listed under Pricing below. |
| 47 | + |
| 48 | +```bash |
| 49 | +git clone https://github.com/devilscrapes/github-user-scraper-javascript.git |
| 50 | +cd github-user-scraper-javascript |
| 51 | +npm install |
| 52 | +export APIFY_TOKEN=your_token_here |
| 53 | +node index.js # uses input.json |
| 54 | +node index.js my-input.json --out data --max-charge 2 |
| 55 | +``` |
| 56 | + |
| 57 | +The script starts the [GitHub User Scraper — Profiles, Repos & Orgs](https://apify.com/DevilScrapes/github-user-scraper), waits for it to finish, prints a link to the run in Apify Console and saves every result to `results.json` and `results.csv`. It exits with code 1 and the run's status message if the run fails. |
| 58 | + |
| 59 | +## 🧩 The core call |
| 60 | + |
| 61 | +Here is the contract in four statements. `call()` resolves once the run has stopped; `listItems()` then returns the dataset rows as plain JavaScript objects. |
| 62 | + |
| 63 | +```javascript |
| 64 | +import { ApifyClient } from 'apify-client'; |
| 65 | + |
| 66 | +const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' }); |
| 67 | +const run = await client.actor('DevilScrapes/github-user-scraper').call({"usernames": ["apify", "microsoft"], "includeRepos": true, "maxReposPerUser": 5, "proxyConfiguration": {"useApifyProxy": false}}); |
| 68 | +const { items } = await client.dataset(run.defaultDatasetId).listItems(); |
| 69 | +console.log(items); |
| 70 | +``` |
| 71 | + |
| 72 | +## 📥 Input |
| 73 | + |
| 74 | +`index.js` loads `input.json` and passes it along unchanged. The sample checks two organizations and keeps five recent repositories each, which comes back as two objects in a short run. Only `usernames` is mandatory. In a service, build the input per request and inject `githubToken` from an environment secret instead of writing it into a file. |
| 75 | + |
| 76 | +```json |
| 77 | +{ |
| 78 | + "usernames": [ |
| 79 | + "apify", |
| 80 | + "microsoft" |
| 81 | + ], |
| 82 | + "includeRepos": true, |
| 83 | + "maxReposPerUser": 5, |
| 84 | + "proxyConfiguration": { |
| 85 | + "useApifyProxy": false |
| 86 | + } |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +| Field | Type | Required | Default | Allowed values | |
| 91 | +|---|---|---|---|---| |
| 92 | +| `usernames` | array | yes | `["torvalds", "apify"]` | — | |
| 93 | +| `githubToken` | string | no | — | — | |
| 94 | +| `includeRepos` | boolean | no | `false` | — | |
| 95 | +| `maxReposPerUser` | integer | no | `30` | — | |
| 96 | +| `concurrency` | integer | no | `6` | — | |
| 97 | +| `proxyConfiguration` | object | no | `{"useApifyProxy": false}` | — | |
| 98 | + |
| 99 | +## 📤 Output |
| 100 | + |
| 101 | +Expect one object per account. `login`, `type` and `html_url` are always set; other profile properties may be null. Counts are numbers, timestamps are ISO strings, and `repos` is either null or an array of objects with fields such as `full_name`, `language`, `stargazers_count` and `pushed_at`. |
| 102 | + |
| 103 | +| Field | Type | Always present | |
| 104 | +|---|---|---| |
| 105 | +| `login` | string | yes | |
| 106 | +| `type` | string | yes | |
| 107 | +| `name` | string or null | no | |
| 108 | +| `company` | string or null | no | |
| 109 | +| `blog` | string or null | no | |
| 110 | +| `location` | string or null | no | |
| 111 | +| `email` | string or null | no | |
| 112 | +| `bio` | string or null | no | |
| 113 | +| `twitter_username` | string or null | no | |
| 114 | +| `public_repos` | integer | no | |
| 115 | +| `public_gists` | integer | no | |
| 116 | +| `followers` | integer | no | |
| 117 | +| `following` | integer | no | |
| 118 | +| `html_url` | string | yes | |
| 119 | +| `avatar_url` | string | no | |
| 120 | +| `hireable` | boolean or null | no | |
| 121 | +| `created_at` | string | no | |
| 122 | +| `updated_at` | string | no | |
| 123 | +| `repos` | array or null | no | |
| 124 | +| `scraped_at` | string | no | |
| 125 | + |
| 126 | +## 💰 Pricing |
| 127 | + |
| 128 | +| Event | Price (USD) | |
| 129 | +|---|---| |
| 130 | +| Actor start | $0.2 | |
| 131 | +| Result emitted | $0.003 | |
| 132 | + |
| 133 | +Prices as configured on 2026-08-20. The [Actor page](https://apify.com/DevilScrapes/github-user-scraper) always shows the current price. Runs are billed to the Apify account that owns the token. Use `--max-charge` to cap what a single run can cost. |
| 134 | + |
| 135 | +## 🛠️ Working with the data in JavaScript |
| 136 | + |
| 137 | +This script compares today's `results.json` with the copy saved last time, reports follower and repository changes per account plus the latest repository push, and then stores the current file for the next comparison. It uses only Node.js built-ins. |
| 138 | + |
| 139 | +```javascript |
| 140 | +import { readFile, writeFile } from 'node:fs/promises'; |
| 141 | + |
| 142 | +const current = JSON.parse(await readFile('results.json', 'utf8')); |
| 143 | +let previous = []; |
| 144 | +try { |
| 145 | + previous = JSON.parse(await readFile('previous.json', 'utf8')); |
| 146 | +} catch { |
| 147 | + // First run: nothing to compare against yet. |
| 148 | +} |
| 149 | + |
| 150 | +const before = new Map(previous.map((acc) => [acc.login, acc])); |
| 151 | +const report = current.map((acc) => { |
| 152 | + const old = before.get(acc.login); |
| 153 | + const pushes = (acc.repos ?? []).map((repo) => repo.pushed_at).filter(Boolean).sort(); |
| 154 | + return { |
| 155 | + login: acc.login, |
| 156 | + type: acc.type, |
| 157 | + followers: acc.followers, |
| 158 | + followerChange: old ? acc.followers - old.followers : null, |
| 159 | + repoChange: old ? acc.public_repos - old.public_repos : null, |
| 160 | + lastPush: pushes.at(-1) ?? null, |
| 161 | + }; |
| 162 | +}); |
| 163 | + |
| 164 | +console.table(report); |
| 165 | +await writeFile('previous.json', JSON.stringify(current, null, 2)); |
| 166 | +``` |
| 167 | +
|
| 168 | +## ❓ FAQ |
| 169 | +
|
| 170 | +### How many accounts can one request handle? |
| 171 | +
|
| 172 | +That depends on GitHub's allowance rather than the Actor. Anonymous access gets 60 requests an hour per address; a token in `githubToken` raises it to 5,000. Each account is one request, and `includeRepos` adds a second. |
| 173 | +
|
| 174 | +### Can my API look up a handle on demand? |
| 175 | +
|
| 176 | +Yes. Start a run with a one-item `usernames` array from your route handler and await it, or, if your platform has short timeouts, use `start()` and pick up the dataset from an Apify webhook when the run succeeds. |
| 177 | +
|
| 178 | +### What does `concurrency` control? |
| 179 | +
|
| 180 | +How many lookups the Actor keeps in flight at once, from 1 to 32 with 6 as the default. Lower it if you share a token with other jobs and want to leave headroom in its hourly budget. |
| 181 | +
|
| 182 | +### Are private repositories or org members included? |
| 183 | +
|
| 184 | +No. Rows contain public profile data only, and the `repos` array lists public repositories. Private membership and private code need scopes this read-only lookup does not use. |
| 185 | +
|
| 186 | +## 🧯 Troubleshooting |
| 187 | +
|
| 188 | +- **`Set APIFY_TOKEN first`**: export the token in the same shell, or put it in a local `.env` file your shell loads. Never commit it. |
| 189 | +- **Run finished with `FAILED` or `ABORTED`**: open the printed Console link; the run log says why. Input validation errors name the field. |
| 190 | +- **Fewer rows than expected**: a `--max-charge` cap also limits how many results can be charged. Raise the cap or lower the limits in your input. |
| 191 | +- **Something looks wrong in the data**: open an issue on the [Actor page](https://apify.com/DevilScrapes/github-user-scraper) so the maintainers see it. |
| 192 | +
|
| 193 | +## 🔗 More from Devil Scrapes |
| 194 | +
|
| 195 | +- [GitHub User Scraper — Profiles, Repos & Orgs on Apify Store](https://apify.com/DevilScrapes/github-user-scraper) |
| 196 | +- [Python version of this example](https://github.com/devilscrapes/github-user-scraper-python) |
| 197 | +- [All Devil Scrapes Actors](https://apify.com/DevilScrapes) |
| 198 | +- [Apify web scraping cookbook](https://github.com/devilscrapes/apify-web-scraping-cookbook) |
| 199 | +
|
| 200 | +Related Actors: |
| 201 | +
|
| 202 | +- [GitHub Organization Scraper](https://apify.com/DevilScrapes/github-org-scraper) — organization metadata plus its public repository list, including member counts. |
| 203 | +- [GitHub Repo Scraper](https://apify.com/DevilScrapes/github-repo-scraper) — deep metadata for individual repositories, such as topics, license and latest release. |
| 204 | +- [GitHub Trending Scraper](https://apify.com/DevilScrapes/github-trending-scraper) — what is climbing GitHub's trending page today. |
| 205 | +
|
| 206 | +--- |
| 207 | +
|
| 208 | +MIT licensed example code. The Actor itself runs on [Apify](https://apify.com/DevilScrapes/github-user-scraper) and is maintained by [Devil Scrapes](https://apify.com/DevilScrapes). Not affiliated with GitHub or Microsoft. |
0 commit comments