mrkeyoor.com_
Thu 06 Aug 07:41 UTC
npmWeb Frontendupdated 06 Aug 2026

superagent

superagent is an HTTP client with a chained, fluent API that runs in both Node and the browser from the same source. Instead of passing a config object you build the request by calling methods in a chain: superagent.post(url).set(header).query(params).send(body).timeout(ms).retry(2), then await it. It was one of the first JavaScript HTTP clients, written by TJ Holowaychuk, and it is now maintained by the Forward Email team. Compared with fetch it hands you a lot of things already assembled: automatic JSON and urlencoded body serialization, response parsing by content type, multipart uploads via .field() and .attach(), a cookie-preserving agent for session flows, upload and download progress events, opt-in retries with a sensible list of retryable status and error codes, and a .use() plugin hook. Its biggest role today is not as a direct dependency but as the engine underneath supertest, which is how a large share of Node API test suites make requests.

Verdict

Keep it where it already lives, especially in test suites built on supertest, because the API is stable and does a lot for you. For new code, the nine dependencies, missing types, and a release line that has been quiet since January 2026 make axios or plain fetch the easier call.

API stability5/5The chained builder has barely moved since the 4.x days, .end() callbacks from a decade ago still work alongside await, and the README's own upgrade notes describe breaking changes as confined to rarely used behavior and stricter error handling.
Docs3/5The docs site covers the whole chain with examples and the README is honest about supported platforms and upgrade paths, but the reference is one long page with no search, TypeScript usage is not documented at all, and details like which status codes .retry() covers are only findable in the source.
Maintenance2/510.3.0 shipped 6 January 2026 and the repo has not been pushed since, releases have thinned to roughly one or two a year, and 179 open issues (185 counting PRs) sit against a project with 16.6k stars; it is being kept alive for Forward Email's own use rather than actively developed.
Ecosystem4/5About 22.9M weekly downloads, mostly arriving transitively through supertest, plus a long list of superagent-* plugins for caching, prefixing, throttling, and mocking; most of those plugins have not been updated in years.

Use it if

  • You are already using supertest for API tests and want the same request builder in application code, since the chain, the plugins, and the response shape are identical
  • You want retries without another package: .retry(3) covers ETIMEDOUT, ECONNRESET, ECONNREFUSED, ENOTFOUND and status codes 408, 413, 429, and the 5xx family out of the box
  • You need multipart uploads with mixed fields and files, where .field('caption', x).attach('file', path) reads better than assembling FormData by hand
  • You need a client that keeps cookies across requests, which superagent.agent() gives you for login-then-do-something flows without a separate cookie jar
  • You want separate response, deadline, and upload timeouts rather than one number covering the whole request
  • You are maintaining an older codebase that already uses the .end(callback) style and want to migrate to promises gradually, because both work on the same request object
Skip it if

Setup reality

npm install superagent works with no config and no peer dependencies, and then two things need attention. First, types: nothing ships in the package, so a TypeScript project needs npm install -D @types/superagent as well, and the community types occasionally lag the runtime. Second, promise semantics. The request object is a thenable, not a real Promise, and it fires the request when you await it or call .end(). Calling both .end() and .then() sends the request twice and the library prints a warning telling you so. Awaiting the same request object twice returns the cached promise rather than repeating the call, which is fine but surprising if you expected a fresh request. Non-2xx responses reject, and the error carries .status and .response, so a 404 lands in your catch block rather than in the happy path. In the browser, the readme recommends a polyfill bundle for WeakRef and BigInt on older Safari and Opera. Finally, the minimum supported Node in package.json is 14.18.0, which tells you how long this API surface has been frozen.

Patterns

Make a GET request and read JSONget-json

const superagent = require('superagent');

const res = await superagent
  .get('https://api.example.com/users')
  .query({ page: 2, active: true })
  .set('Accept', 'application/json');

res.status;   //=> 200
res.body;     //=> parsed JSON
res.headers;  //=> lowercased header map

res.body is the parsed object and res.text is the raw string; body is only populated for content types superagent knows how to parse. Awaiting the request is what sends it, so a request you build but never await never leaves the process.

POST a JSON bodypost-json

const res = await superagent
  .post('/api/pet')
  .send({ name: 'Manny', species: 'cat' })
  .set('X-API-Key', 'foobar')
  .accept('json');

res.body.id;

Passing a plain object to .send() sets Content-Type to application/json for you. Calling .send() twice merges objects, which is handy for defaults but silently overwrites keys you did not intend to change.

Tell an HTTP error from a network errorhandle-errors

try {
  await superagent.get('/thing');
} catch (err) {
  if (err.response) {
    console.error('HTTP', err.status, err.response.body);
  } else if (err.timeout) {
    console.error('timed out after', err.timeout, 'ms');
  } else {
    console.error('network', err.code);
  }
}

Any non-2xx rejects, so a 404 you expect has to be caught. err.response exists only when the server actually answered. Timeouts set err.timeout to the millisecond value and err.code to ECONNABORTED, with err.errno carrying ETIME or ETIMEDOUT depending on which timer fired.

Treat some non-2xx responses as successaccept-status

const res = await superagent
  .get('/maybe-missing')
  .ok(res => res.status < 500);

if (res.status === 404) {
  return null;
}

Without .ok(), every 4xx becomes a thrown error and you end up writing try/catch around lookups that legitimately return 404. The callback is required; passing a non-function throws immediately.

Set separate response and deadline timeoutstimeouts

await superagent
  .get('/slow-report')
  .timeout({
    response: 5000,   // waiting for the first byte
    deadline: 60000,  // whole request including download
  });

There is no timeout by default, so a hung server holds the socket forever. Passing a plain number sets only the deadline; the response timeout is what actually protects you from a server that accepts the connection and then stalls.

Retry transient failuresretry-requests

const res = await superagent
  .get('/flaky')
  .timeout({ response: 5000, deadline: 20000 })
  .retry(3, (err, res) => {
    if (res && res.status === 401) return false; // never retry auth
  });

Built-in retries cover ETIMEDOUT, ECONNRESET, ECONNREFUSED, ENOTFOUND, EPIPE and statuses 408, 413, 429, 500, 502, 503, 504, 521, 522, 524. Note that it retries any method including POST, so guard non-idempotent calls with the callback.

Upload files alongside form fieldsupload-multipart

const res = await superagent
  .post('/upload')
  .field('caption', 'invoice scan')
  .field('tags', JSON.stringify(['ap', '2026']))
  .attach('document', '/tmp/invoice.pdf')
  .on('progress', event => {
    if (event.direction === 'upload' && event.total) {
      console.log(event.loaded, 'of', event.total);
    }
  });

Do not set Content-Type yourself; superagent builds the multipart boundary. The event payload differs by runtime: the browser build adds a percent field, the Node build gives you only direction, lengthComputable, loaded, and total, and total is undefined when Content-Length is unknown.

Keep cookies across requestscookie-session

const superagent = require('superagent');
const agent = superagent.agent();

await agent.post('/login').send({ user: 'ada', pass: 'secret' });
const me = await agent.get('/account/me');

superagent.agent() stores the session cookie and replays it, which plain superagent calls do not do. In the browser you want .withCredentials() instead, plus matching CORS headers on the server.

Send a urlencoded body instead of JSONform-urlencoded

await superagent
  .post('/oauth/token')
  .type('form')
  .send({ grant_type: 'client_credentials', scope: 'read' });

.type('form') switches the serializer to application/x-www-form-urlencoded, which is what most token endpoints require. Arrays are encoded with qs indices semantics, so tags[0]=a&tags[1]=b rather than repeated keys.

Cancel an in-flight requestabort-request

const req = superagent.get('/big-export');
const promise = req.then(res => res.body);

setTimeout(() => req.abort(), 2000);

try {
  await promise;
} catch (err) {
  if (err.code === 'ABORTED') console.log('cancelled');
}

There is no AbortController support; you hold the request object and call .abort(). The rejection carries code ABORTED unless the request had already timed out, in which case you get the timeout error instead.

Share configuration with a pluginplugin-defaults

const withDefaults = req => {
  req.set('Authorization', `Bearer ${getToken()}`);
  req.timeout({ response: 5000, deadline: 15000 });
  req.retry(2);
};

const res = await superagent.get('/orders').use(withDefaults);

.use() is the only shared-config hook and it runs against the request before it is sent, so there is no place to normalize responses globally. Wrap superagent in your own factory function if you need that.

Pipe a response to disk in Nodestream-download

const fs = require('node:fs');

const req = superagent
  .get('https://example.com/big.zip')
  .maxResponseSize(500 * 1024 * 1024);

req.pipe(fs.createWriteStream('/tmp/big.zip'));
req.on('end', () => console.log('done'));

Use .pipe() instead of awaiting, because awaiting buffers the whole body in memory first. Never mix .pipe() with .end() or await on the same request, and set .maxResponseSize() so an unexpected multi-gigabyte body cannot take the process down.

Alternatives

PackageRegistryPick it when
axiosnpmYou want the same batteries-included feel with request and response interceptors, shipped TypeScript types, and a far more active release cadence
kynpmYou are browser-first on modern runtimes and want retries and hooks in a tiny fetch wrapper with no dependency tree
gotnpmYou are Node-only and want streams, hooks, pagination, and a much more configurable retry policy