adsabs-client
v1.0.0
Published
A fully-featured, production-ready TypeScript/Node.js client for the SAO/NASA Astrophysics Data System (ADS) API v1.
Maintainers
Readme
adsabs-client
A fully-featured, production-ready TypeScript/Node.js client for the SAO/NASA Astrophysics Data System (ADS) API v1.
Features
- Zero external runtime dependencies — uses native
fetch(Node ≥ 18) - Full API coverage — search, export, metrics, libraries, journals, resolver, objects, oracle, visualizations, accounts, citation helper, feedback
- Immutable query builder — composable, type-safe Solr query construction
- Lazy async iteration —
for awaitover millions of results with automatic cursor pagination - Automatic retry — full-jitter exponential back-off on 429 / 5xx
- Rate-limit tracking — parses
X-RateLimit-*headers; fires a warning callback when quota runs low - Concurrent batch helpers —
searchAll,fetchMetrics,exportAll,resolveUrls,asyncMapwith semaphore-bounded concurrency - Structured errors — every failure is an
AdsErrorwith a typedkinddiscriminant - Dual ESM + CJS — tree-shakeable ESM and CommonJS bundles with full
.d.tsdeclarations
Installation
npm install adsabs-clientRequires Node.js ≥ 18 (for native fetch).
Get your API token at https://ui.adsabs.harvard.edu/user/settings/token.
Quick start
import { AdsClient } from 'adsabs-client';
const client = new AdsClient({ apiToken: process.env.ADS_API_TOKEN! });
// Simple string search
const resp = await client.search.query('gravitational waves', { rows: 5 });
console.log(resp.numFound, resp.docs[0]);
// Stream all results lazily (no memory blow-up)
for await (const doc of client.search.stream('neutron stars')) {
console.log(doc['bibcode'], doc['title']);
}
// Export to BibTeX
const bibtex = await client.export.bibtex(['2016PhRvL.116f1102A']);Query builder
The Query class lets you compose Solr queries without string manipulation.
Every method returns a new instance — builders are immutable and safe to share.
import { Query, SortDirection } from 'adsabs-client/query';
const q = new Query()
.author('Hawking, S')
.yearRange(1970, 2018)
.property('refereed')
.fields('title', 'bibcode', 'citation_count', 'author')
.sort('citation_count', SortDirection.Desc)
.rows(20);
const resp = await client.search.query(q);API reference
AdsClient constructor
const client = new AdsClient({
apiToken: 'your_token', // required
baseUrl: 'https://...', // default: ADS production
maxAttempts: 4, // default: 4 (1 + 3 retries)
baseBackoffMs: 500, // default: 500ms
maxBackoffMs: 30_000, // default: 30s
rateLimitWarnThreshold: 100, // fire onRateLimitWarning below this
onRateLimitWarning: (info) => console.warn('Quota low:', info.remaining),
onRetry: (attempt, reason, ms) => console.log(`Retry ${attempt} in ${ms}ms`),
});Services
| client.search | /search/query, /search/bigquery |
|---|---|
| .query(q, opts?) | Run a search; q is a string or Query |
| .bigquery(bibcodes, opts?) | Efficient bulk bibcode lookup |
| .citations(bibcodes, opts?) | Papers that cite these bibcodes |
| .references(bibcodes, opts?) | Papers referenced by these bibcodes |
| .trending(q, opts?) | Papers trending alongside a query |
| .stream(q, opts?) | AsyncIterable<Doc> — lazy cursor pagination |
| client.export | /export/:format |
|---|---|
| .bibtex(bibcodes, opts?) | BibTeX |
| .ris(bibcodes, opts?) | RIS |
| .endnote(bibcodes, opts?) | EndNote |
| .aastex / .mnras / .icarus / .soph / .refabsxml / .rss | … |
| .custom(bibcodes, template, opts?) | Custom format template |
| client.metrics | /metrics |
|---|---|
| .fetch(bibcodes) | All metrics |
| .basic / .citations / .indicators / .histograms / .timeSeries | Typed subsets |
| client.libraries | /biblib/* |
|---|---|
| .list() | All libraries for the authenticated user |
| .get(id, opts?) | Library with bibcodes |
| .create(name, opts?) | Create a library |
| .update(id, updates) | Rename, change visibility, etc. |
| .delete(id) | Permanently delete |
| .addDocuments(id, bibcodes) | Add papers |
| .removeDocuments(id, bibcodes) | Remove papers |
| .permissions(id) | List permissions |
| .setPermission(id, email, perm) | Grant/revoke access |
| .transfer(id, email) | Transfer ownership |
| .setOperation(id, op, opts?) | union / intersection / difference / copy / empty |
| client.journals | /journals/* |
|---|---|
| .summary(bibstem) | Journal summary |
| .journal(bibstem) | Full details |
| .volume(bibstem, vol) | Volume details |
| .byIssn(issn) | Lookup by ISSN |
| .holdings(bibstem) | ADS holdings |
| .list(opts?) | Paginated journal list |
| client.resolver | /resolver/:bibcode |
|---|---|
| .resolve(bibcode) | All available resource links |
| .resolveType(bibcode, linkType) | Specific link type |
| .fullTextUrl(bibcode) | Publisher PDF/HTML URL |
| .preprintUrl(bibcode) | arXiv URL |
| Other services | |
|---|---|
| client.objects | .resolve, .query, .resolveMany |
| client.oracle | .alsoRead, .matchDocument |
| client.vis | .authorNetwork, .paperNetwork, .wordCloud |
| client.accounts | .check(), .validateToken(), .rateLimitStatus() |
| client.citationHelper | .suggest(opts) |
| client.feedback | .submit(opts) |
| client.pagination | .count(q), .collectAll(q, opts?), .pages(q, opts?) |
Async batch helpers
import { searchAll, fetchMetrics, exportAll, resolveUrls, asyncMap } from 'adsabs-client';
// Run 3 queries concurrently (max 3 parallel by default)
const results = await searchAll(client.search, [
'black holes',
'neutron stars',
'gravitational waves',
]);
for (const r of results) {
if (r.error) console.error(r.query, r.error);
else console.log(r.query, r.result.numFound);
}
// Generic map with bounded concurrency
const urls = await asyncMap(bibcodes, (b) => client.resolver.fullTextUrl(b), {
maxConcurrency: 5,
});Error handling
Every failure throws an AdsError with a typed kind field:
import { AdsError, ErrorKind } from 'adsabs-client';
try {
await client.search.query('stars');
} catch (err) {
if (err instanceof AdsError) {
switch (err.kind) {
case ErrorKind.RateLimited:
await delay(err.retryAfterMs);
break;
case ErrorKind.Unauthorized:
throw new Error('Check your ADS_API_TOKEN');
case ErrorKind.NotFound:
console.log('Record not found');
break;
}
}
}Pagination
// Count only (1 request, rows=0)
const total = await client.pagination.count('exoplanets');
// Collect all into memory (use carefully for large sets)
const docs = await client.pagination.collectAll('pulsars', { maxResults: 500 });
// Page-by-page async iteration
for await (const page of client.pagination.pages('quasars', { rows: 100 })) {
console.log(`Got ${page.docs.length} docs (${page.numFound} total)`);
// page also has: facets, highlights, rateLimit, nextCursorMark
}Rate-limit monitoring
const client = new AdsClient({
apiToken: '...',
rateLimitWarnThreshold: 200,
onRateLimitWarning: (info) => {
console.warn(`ADS quota low: ${info.remaining}/${info.limit} remaining`);
},
});
// Check current quota at any time (no HTTP request)
const status = client.rateLimitStatus();
console.log(status.remaining, new Date(status.resetAt));Development
npm install
npm test # run all 272 tests
npm run test:coverage # with v8 coverage report
npm run build # emit ESM + CJS + .d.ts to dist/
npm run typecheck # tsc --noEmitLicense
Apache-2.0 © iamkanishka
