# How to stub outbound HTTP calls in Node tests with nock

> Intercept outbound requests in-process, assert on what your code sent as well as received, and turn off real network access so an unstubbed call fails.

Source: https://voxgig.com/howto/stub-outbound-node-http-calls-with-nock

- Audience: api-consumer
- Level: intermediate
- Languages: typescript, javascript
- Verified: 2026-09-06
- Published: 2026-09-06

## Short answer

Call nock.disableNetConnect once for the suite, then declare an interceptor per test with the host, path, and expected request headers. Assert with isDone that the interceptor was used, so a test cannot pass because your code never made the call. Clean interceptors between tests, because one left behind answers a request in a later test.

---
## You will need

Node 22 or later, and code that makes outbound calls. Version 14 of
[nock](https://github.com/nock/nock) intercepts the global fetch as well as `http.request`, which
matters because a mock that only covers one of those leaves half a modern codebase unstubbed.

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [A local test server](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) | You want no mocking library and full control of the responses | Your code has to accept a base URL, and every test manages a port and a lifecycle | The call is buried in a dependency you cannot point elsewhere |
| [MSW](https://mswjs.io/) | The same handlers should serve browser tests, Node tests and local development | A larger tool with its own handler model, which is more to learn for a backend-only suite | Only Node tests need stubbing |
| [nock](https://github.com/nock/nock) | Node suites that want per-test stubs and assertions on the request | Interceptors are consumed once, so a retry test needs the stub declared as many times | The code under test talks to a database or a queue rather than HTTP |
| [undici MockAgent](https://undici.nodejs.org/#/docs/api/MockAgent) | You already build requests through an undici dispatcher | It covers undici's own paths, so a library using node:http directly is untouched | Calls are made by several different clients |

The local server and the interceptor differ in what they can reach. A server needs the code under
test to be pointed at it, which a well-designed client allows and a third-party dependency usually
does not. An interceptor works at the HTTP layer, so it catches calls from code you cannot configure.

## Turn the network off first

The single most valuable line is the one that makes an unstubbed call an error.

```ts title="nock.test.mjs"
before(() => {
  // Any request the test suite did not stub is a real network call, and a test
  // suite that can reach the internet is a test suite that fails at somebody
  // else's deploy. This makes an unstubbed call an error instead.
  nock.disableNetConnect()
})
beforeEach(() => nock.cleanAll())
after(() => { nock.cleanAll(); nock.enableNetConnect() })
```

Without it, a call your stubs do not match goes to the real host. The test then either passes
because the vendor answered, or fails with a network error that reads like a bug in your code. With it, the
same call fails immediately and names the host it tried to reach.

Cleaning between tests is the other half. An interceptor that a test declared and never used stays
armed, and the next test that happens to call the same path gets the previous test's response. That
failure is hard to find because it depends on test order.

## Assert on what you sent

A stub is a chance to check the request, not only to supply a response.

```ts title="nock.test.mjs"
test('the stub asserts on the request as well as answering it', async () => {
  const scope = nock('https://api.example.com', {
    reqheaders: { authorization: 'Bearer sk-test-1' },
  })
    .get('/v1/invoices/in_2')
    .reply(200, { id: 'in_2' })

  await fetchInvoice('in_2')

  assert.equal(scope.isDone(), true, 'the interceptor was used')
})
```

The `reqheaders` option makes the interceptor match only when the header is present and correct. A
request missing it does not match, so it falls through to the blocked network and fails. The header
assertion and the response stub are the same declaration.

`isDone` is what stops a false pass. Without it a test that never calls the API still succeeds,
because nothing checks that the interceptor was reached, and a refactor that removes the call
entirely leaves the suite green.

## Check it worked

Four tests: a stubbed response, an assertion on the request, a 404 the client maps to null, and a
call nobody stubbed.

```bash
node --test nock.test.mjs
```

```text output
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 228.192563
```

The fourth test is the one worth reading twice. It asserts that an unstubbed call rejects, which
proves the suite is closed rather than assuming it.

Running the same file with the network left on is the way to see the difference. Comment out
`disableNetConnect`, change one path so the stub no longer matches, and the request leaves the
machine. Whether that fails depends on the network and on the vendor, which is the property a test
suite should never have.

The other thing to check once is the interception surface. A dependency that uses [node:http](https://nodejs.org/api/http.html)
directly, rather than the global fetch, is covered by the same interceptors. That is the reason to
prefer an HTTP-layer mock over passing a fake client into your own code.

## When it goes wrong

A retry test passes on the first attempt and fails on the second. An interceptor answers one request
and is then spent, so code that retries needs the stub declared for each attempt, or `.times(3)`.
The failure looks like your retry logic misbehaving when the stub ran out instead.

The second failure is a stub that does not match and gives no reason. A path with a query string, a
trailing slash, or a body that differs by whitespace all fail to match, and the visible result is
only that the request escaped. Print `nock.pendingMocks()` in the failing test, which lists the
interceptors still waiting and the exact shapes they expect.

The third is a suite that stubs so precisely it tests the stub. A test asserting on a fixed response
body it wrote itself proves that a constant equals a constant. Keep the stub thin and assert on what
your code did with it.

## When not to do this

Do not stub the API in every test in the suite. The stubs encode your belief about the vendor, and a
suite made only of stubs passes whatever the vendor is doing, which is what a scheduled contract
check exists to cover.

Do not use interceptors to test your own service's endpoints. Those you can call for real, in
process, and a stub between two pieces of your own code hides the integration that is worth testing.

Do not leave `disableNetConnect` out because one test needs the network. Allow that host explicitly
with `nock.enableNetConnect(host)`, so the exception is visible and narrow rather than a suite-wide
gap nobody remembers.

Do not record stubs from production traffic without reading them. A recorded interaction carries
whatever headers and identifiers were in flight, and those end up committed to a repository.

## Related how-tos

- [Contract test an API you do not own](/howto/contract-test-an-api-you-do-not-own)

- [Choose consumer-driven or spec-driven contract tests](/howto/choose-consumer-driven-or-spec-driven-contract-tests)

## Last verified

Verified 2026-09-06 against Node 22.22.2 and nock 14.0.10. The output block is what the preceding
command printed, with the network turned off for the whole run.