> ## Content Index
> Fetch the complete content index at: https://roundproxies.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Use Got: Making HTTP Requests in Node.js
- URL: https://roundproxies.com/blog/got/
- Published: 2025-09-29T22:42:44.000Z
- Updated: 2026-09-23T13:04:15.000Z
- Description: Learn Got, the Node.js HTTP library with built-in retries, streams, and error handling. Better than fetch for APIs, large files, and pagination.
- Author: Marius Bernard
- Tags: #dated-68daff084fa498a2c6ee1cc7

Got is a human-friendly HTTP request library for Node.js that handles retries, streams, and errors out of the box. Unlike fetch or axios, Got ships with automatic retry logic, built-in cookie support, and pagination for APIs that return paginated data.

This guide walks you through setting up Got, from basic requests to streaming large files and handling rate limits.

## Why Got Beats Other HTTP Libraries

Got solves problems that other libraries ignore. Most HTTP clients make you handle retries, parse cookies, and deal with stream errors yourself. Got does this automatically.

Here's what sets it apart: automatic retries on network failures (up to 2 times by default), built-in cookie jar support, RFC-compliant caching, and stream error handling that actually works. Companies like Segment use it to power their backend API, handling millions of requests daily.

The catch? Got is **ESM-only** starting from v12\. No CommonJS `require()` support. This trips up a lot of developers, but we'll fix that in step 1.

## Step 1: Install and import Got (ESM setup)

Installing is one command:

```bash
npm install got

```

The import is where people get stuck. Got is native ESM from v12 onward, so a Node.js project that still runs on CommonJS cannot `require('got')`. Your first HTTP request fails before a single packet leaves the machine, usually with an `ERR_REQUIRE_ESM` error pointing at the Got entry file. Pick one of the three paths below before you write any request code.

### If you're using ES modules (package.json has "type": "module")

```javascript
import got from 'got';

const response = await got('https://api.github.com/users/sindresorhus');
console.log(response.body);

```

Nothing else to configure. Two details worth noticing in those four lines: the `await` sits at the top level of the file, which only ESM allows, and `response.body` is a raw string, not a parsed object. Got has a `.json()` shortcut for that, which comes up in the next step.

### If you're stuck with CommonJS

You have two options. Option 1 is using dynamic imports:

```javascript
// myfile.js (CommonJS)
async function fetchData() {
  const got = (await import('got')).default;
  const response = await got('https://api.github.com/users/sindresorhus');
  return response.body;
}

fetchData().then(console.log);

```

The `await import()` loads Got asynchronously. You need to grab `.default` because Got exports as a default export. Two caveats: the import has to live inside an async function, and it runs on every call unless you cache the module in a variable outside the function. For a script that fires one request, nobody cares. For a server handling traffic, hoist it.

Option 2 is staying on Got v11 (the last CommonJS version):

```bash
npm install got@11

```

```javascript
const got = require('got');

(async () => {
  const response = await got('https://api.github.com/users/sindresorhus');
  console.log(response.body);
})();

```

Got v11 still works fine for most cases, but you miss out on newer features. It is also frozen: bug fixes and protocol work land on the current release line only, so pinning to v11 means inheriting whatever it shipped with, forever. I'd take the dynamic import over the version pin. Pinning is the choice that ages badly.

### Converting a CommonJS project to ESM

For new projects, just switch to ESM by adding `"type": "module"` to your package.json. On an existing codebase that flag flips the meaning of every `.js` file at once, so expect a short cleanup pass:

- Every `require()` becomes an `import`, and every `module.exports` becomes an `export`.
- `__dirname` and `__filename` stop existing. Rebuild them from `import.meta.url`.
- Relative imports need the file extension: `./utils.js`, not `./utils`.
- Any file you are not ready to convert can keep CommonJS by renaming it to `.cjs`.

**Quick recommendation.** Greenfield project, go ESM and import Got normally. Legacy CommonJS service you cannot touch this sprint, use the dynamic import and move on.

## Step 2: Make your first requests

Got supports all HTTP methods. Here's how to use them:

### GET requests (the simplest case)

```javascript
import got from 'got';

const response = await got('https://api.github.com/users/octocat');
console.log(response.body); // Raw HTML/JSON string
console.log(response.statusCode); // 200

```

The response object contains everything: body, status code, headers, timing data. But here's a trick most developers miss: Got has shorthand methods that parse responses automatically:

```javascript
// Instead of this:
const response = await got('https://api.github.com/users/octocat', {
  responseType: 'json'
});
console.log(response.body);

// Do this:
const data = await got('https://api.github.com/users/octocat').json();
console.log(data); // Already parsed JSON

```

The `.json()` method parses the response and returns just the data. No need to access `.body`. It also works with `.text()` and `.buffer()`.

### POST requests with JSON

```javascript
const data = await got.post('https://httpbin.org/post', {
  json: {
    username: 'testuser',
    email: 'test@example.com'
  }
}).json();

console.log(data);

```

The `json` option automatically sets `Content-Type: application/json` and stringifies your object. No manual `JSON.stringify()` needed.

### Custom headers and query params

```javascript
const data = await got('https://api.github.com/search/repositories', {
  searchParams: {
    q: 'got',
    sort: 'stars',
    per_page: 5
  },
  headers: {
    'User-Agent': 'my-app',
    'Authorization': 'token YOUR_TOKEN'
  }
}).json();

console.log(data.items);

```

`searchParams` builds the query string for you. No need to manually encode URLs or concatenate strings. It handles arrays and nested objects too.

Here's something most tutorials don't mention: you can pass a URL object instead of a string:

```javascript
const url = new URL('https://api.github.com/repos/sindresorhus/got');
url.searchParams.set('per_page', '100');

const data = await got(url).json();

```

This is useful when you're building URLs programmatically or need to manipulate existing URLs.

## Step 3: Handle errors the right way

Got throws different error types depending on what went wrong. Understanding these prevents silent failures:

```javascript
import got, { HTTPError, RequestError } from 'got';

try {
  const data = await got('https://api.github.com/users/nonexistentuser123456').json();
} catch (error) {
  if (error instanceof HTTPError) {
    // Server responded with error status (4xx, 5xx)
    console.log('Status:', error.response.statusCode);
    console.log('Body:', error.response.body);
    console.log('Message:', error.message);
  } else if (error instanceof RequestError) {
    // Network error, timeout, DNS failure
    console.log('Network error:', error.message);
    console.log('Code:', error.code); // ETIMEDOUT, ENOTFOUND, etc.
  } else {
    // Something else broke
    console.log('Unexpected error:', error);
  }
}

```

`HTTPError` means the server responded but with a bad status code. `RequestError` means the request never completed: network issues, timeouts, or DNS problems.

Here's the key difference: with `HTTPError`, you can still access `error.response` to see what the server said. With `RequestError`, there's no response at all.

### Custom error handling with hooks

```javascript
const client = got.extend({
  hooks: {
    beforeError: [
      error => {
        // Add custom context to errors
        const {request, response} = error;
        if (response && response.body) {
          error.message = `${error.message}: ${response.body}`;
        }
        return error;
      }
    ]
  }
});

```

The `beforeError` hook runs right before Got throws an error. You can modify the error message, log details, or even change what error gets thrown.

### Disable automatic error throwing

Sometimes you want to handle bad status codes yourself:

```javascript
const response = await got('https://httpbin.org/status/404', {
  throwHttpErrors: false
});

console.log(response.statusCode); // 404
console.log(response.ok); // false

if (!response.ok) {
  // Handle error your way
  console.log('Request failed but we handled it gracefully');
}

```

Set `throwHttpErrors: false` and Got won't throw on 4xx/5xx responses. Useful when you're scraping and want to log failed requests without crashing.

## Step 4: Work with streams and large files

Streams are where Got really shines. Unlike axios, Got has first-class stream support.

### Download large files efficiently

```javascript
import got from 'got';
import {createWriteStream} from 'fs';
import {pipeline} from 'stream/promises';

await pipeline(
  got.stream('https://example.com/large-file.zip'),
  createWriteStream('downloaded.zip')
);

console.log('Download complete');

```

`pipeline()` from `stream/promises` is critical here. It handles backpressure and errors automatically. Never use `.pipe()` for production code, because it doesn't propagate errors properly.

Here's what happens without `pipeline()`:

```javascript
// BAD: This looks fine but has a bug
got.stream('https://example.com/file.zip')
  .pipe(createWriteStream('file.zip'));

// If the download fails halfway, the error won't propagate
// Your write stream never knows the download failed

```

The stream keeps writing even after the download errors out. You end up with a corrupted file and no error.

### Upload files with streams

```javascript
import {createReadStream} from 'fs';

const response = await got.post('https://httpbin.org/post', {
  body: createReadStream('upload.txt')
}).json();

console.log(response);

```

Got automatically sets `Transfer-Encoding: chunked` when you pass a stream. No need to know the file size upfront.

### Track download progress

```javascript
const downloadStream = got.stream('https://example.com/file.zip');

downloadStream.on('downloadProgress', progress => {
  const percent = Math.round(progress.percent * 100);
  console.log(`Downloaded: ${percent}%`);
});

await pipeline(
  downloadStream,
  createWriteStream('file.zip')
);

```

The `downloadProgress` event fires multiple times as data comes in. `progress.percent` is a decimal (0 to 1), `progress.transferred` shows bytes downloaded, and `progress.total` shows total size (if the server sends Content-Length).

### Handle stream errors properly

Stream error handling is tricky. Here's the right way:

```javascript
import {pipeline} from 'stream/promises';

try {
  await pipeline(
    got.stream('https://example.com/file.zip'),
    createWriteStream('file.zip')
  );
  console.log('Success');
} catch (error) {
  console.log('Stream failed:', error.message);
  // Clean up partial file
  fs.unlink('file.zip', () => {});
}

```

`pipeline()` from `stream/promises` returns a promise that rejects on any error. This is way cleaner than attaching `.on('error')` handlers to every stream in the chain.

## Step 5: Set up retries, hooks, and pagination

Got's advanced features are where it separates from other libraries.

### Automatic retries

Got retries failed requests by default. Here's how to configure it:

```javascript
const data = await got('https://unreliable-api.com/data', {
  retry: {
    limit: 3,
    methods: ['GET', 'POST'],
    statusCodes: [408, 413, 429, 500, 502, 503, 504],
    errorCodes: [
      'ETIMEDOUT',
      'ECONNRESET',
      'EADDRINUSE',
      'ECONNREFUSED',
      'EPIPE',
      'ENOTFOUND',
      'ENETUNREACH',
      'EAI_AGAIN'
    ]
  }
}).json();

```

`limit` sets max retries (default is 2). `statusCodes` are HTTP status codes that trigger a retry. `errorCodes` are network error codes (timeouts, DNS failures, etc.).

By default, Got only retries `GET`, `PUT`, `HEAD`, `DELETE`, and `OPTIONS`. It won't retry `POST` unless you explicitly add it to `methods`.

### Custom retry logic

```javascript
const data = await got('https://api.example.com/data', {
  retry: {
    limit: 5,
    calculateDelay: ({attemptCount, retryOptions, error, computedValue}) => {
      // Exponential backoff with jitter
      const baseDelay = 1000;
      const maxDelay = 10000;
      
      const delay = Math.min(
        baseDelay * Math.pow(2, attemptCount),
        maxDelay
      );
      
      // Add random jitter to prevent thundering herd
      const jitter = Math.random() * 1000;
      
      return delay + jitter;
    }
  }
}).json();

```

`calculateDelay` gives you full control over retry timing. `attemptCount` starts at 1\. Return the delay in milliseconds before the next retry.

The default algorithm is: `1000 * Math.pow(2, attemptCount) + Math.random() * 100`. This creates exponential backoff: 1s, 2s, 4s, 8s, etc.

### Use hooks for auth tokens

Hooks let you modify requests before they're sent:

```javascript
let accessToken = 'initial_token';

const client = got.extend({
  hooks: {
    beforeRequest: [
      options => {
        options.headers.authorization = `Bearer ${accessToken}`;
      }
    ],
    afterResponse: [
      (response, retryWithMergedOptions) => {
        if (response.statusCode === 401) {
          // Token expired, refresh it
          accessToken = refreshToken();
          
          // Retry with new token
          return retryWithMergedOptions({
            headers: {
              authorization: `Bearer ${accessToken}`
            }
          });
        }
        
        return response;
      }
    ]
  }
});

// Now all requests use the token automatically
const data = await client('https://api.example.com/protected').json();

```

`beforeRequest` runs before every request. Perfect for adding auth headers.

`afterResponse` runs after getting a response. If you return `retryWithMergedOptions()`, Got retries the request with new options. This is how you implement token refresh logic.

### Pagination for APIs

Many APIs return paginated results. Got handles this automatically:

```javascript
const pagination = got.paginate('https://api.github.com/repos/sindresorhus/got/commits', {
  pagination: {
    countLimit: 50,
    paginate: ({response}) => {
      // GitHub uses Link header for pagination
      const linkHeader = response.headers.link;
      
      if (!linkHeader) {
        return false; // No more pages
      }
      
      // Parse Link header to get next page URL
      const match = linkHeader.match(/<([^>]+)>; rel="next"/);
      if (!match) {
        return false;
      }
      
      return {url: match[1]};
    }
  }
});

for await (const commit of pagination) {
  console.log(commit.commit.message);
}

```

`paginate` is a function that extracts the next page URL from each response. Return `false` when there are no more pages. Return an object with `url` for the next page.

`countLimit` stops after fetching N items total. Without it, Got keeps paginating until the API runs out of data.

By default, Got looks for a `Link` header (RFC 5988). For APIs that use different pagination (like `next_page` in JSON), you need a custom `paginate` function:

```javascript
const pagination = got.paginate('https://api.example.com/users', {
  searchParams: {page: 1},
  pagination: {
    paginate: ({response, currentItems}) => {
      const data = JSON.parse(response.body);
      
      if (!data.next_page) {
        return false;
      }
      
      return {
        searchParams: {page: data.next_page}
      };
    }
  }
});

```

## Common mistakes to avoid

### Mistake 1: Using `.pipe()` instead of `pipeline()`

Never chain streams with `.pipe()` in production:

```javascript
// BAD
got.stream(url)
  .pipe(createWriteStream('file.txt'));

```

Errors don't propagate. Use `pipeline()` from `stream/promises`:

```javascript
// GOOD
await pipeline(
  got.stream(url),
  createWriteStream('file.txt')
);

```

### Mistake 2: Not handling 404s gracefully

By default, Got throws on 404\. If you're checking if a resource exists, handle it:

```javascript
// Check if user exists
try {
  await got(`https://api.github.com/users/${username}`);
  console.log('User exists');
} catch (error) {
  if (error instanceof HTTPError && error.response.statusCode === 404) {
    console.log('User not found');
  } else {
    throw error; // Re-throw other errors
  }
}

```

Or use `throwHttpErrors: false` and check `response.ok`.

### Mistake 3: Setting timeouts too low

Network requests are slow. The default timeout is generous for a reason:

```javascript
// Be realistic with timeouts
const data = await got('https://slow-api.com/data', {
  timeout: {
    request: 30000 // 30 seconds
  }
}).json();

```

Got has granular timeouts: `lookup` (DNS), `connect` (TCP), `secureConnect` (TLS), `socket` (inactive socket), `send` (upload), `response` (first byte), and `request` (entire request).

Most of the time, you only need `request` timeout.

### Mistake 4: Not reusing instances

Creating Got instances with `got.extend()` is cheap. Reuse them:

```javascript
// BAD: Repeating options
await got('https://api.example.com/users', {
  headers: {authorization: `Bearer ${token}`}
});
await got('https://api.example.com/posts', {
  headers: {authorization: `Bearer ${token}`}
});

// GOOD: Reuse instance
const client = got.extend({
  prefixUrl: 'https://api.example.com',
  headers: {authorization: `Bearer ${token}`}
});

await client('users').json();
await client('posts').json();

```

`prefixUrl` is especially useful. All requests use it as a base URL.

## Final thoughts

Got handles edge cases that other HTTP libraries ignore. Automatic retries save you from writing retry logic manually. Built-in stream support makes downloading large files trivial. Hooks let you implement auth token refresh without wrapper functions.

The ESM-only requirement is annoying if you're on older projects, but it's the future of Node.js. Most issues come from not understanding how to handle errors in streams or not using `pipeline()` correctly.

For most API work, Got is overkill compared to fetch. But when you need retries, pagination, or streaming, Got is the only library that gets it right without extra dependencies.