nock, undici MockAgent, overrideProvider, and when the mock needs to be a real URLHonest split first: nock 14 is the right default for fast offline unit tests of NestJS services that call external APIs. We verified it intercepts both of Nest's common HTTP paths โ HttpService from @nestjs/axios (axios over Node's http adapter) and Node 22's built-in fetch (nock โฅ14 patches undici). This page is not going to pretend otherwise.
What actually sends people searching are three boundaries: a nasty version-skew trap in undici's own MockAgent that makes tests silently hit the real network, the confusion between overrideProvider (DI rewiring) and HTTP mocking, and the process boundary โ no in-process mock reaches a running nest start server, a Playwright browser, or CI against a deployed preview. All three verified below, on the week of writing (Aug 2026: NestJS 12.0.1, @nestjs/axios 12.0.0, axios 1.20, nock 14.0.17, undici 8.10, Node 22.23 with bundled undici 6.27). Every snippet was actually run.
A Nest service using HttpService, and the same URL fetched with global fetch โ one nock intercept each:
nock('https://api.example.com').get('/products/1').reply(200, { id: 1, name: 'NOCKED' });
// via HttpService (axios) โ INTERCEPTED (verified)
const res = await firstValueFrom(this.http.get('https://api.example.com/products/1'));
// via Node 22 global fetch โ INTERCEPTED (verified โ nock โฅ 14 patches undici)
const r = await fetch('https://api.example.com/products/1');
Both returned the nocked body in our runs. If your whole test fits in one process and one canned answer per URL is enough, stop here โ nock is excellent.
Plenty of tutorials now recommend undici's own MockAgent for mocking fetch. Here's what we measured on Node 22 with undici@8.10 installed from npm โ with disableNetConnect() on:
const mockAgent = new MockAgent();
mockAgent.disableNetConnect(); // "block everything unmocked"
setGlobalDispatcher(mockAgent);
mockAgent.get('https://api.example.com')
.intercept({ path: '/products/1', method: 'GET' })
.reply(200, { id: 1, name: 'UNDICI-MOCKED' });
await fetch('https://api.example.com/products/1');
// โ MISSED. Real network. Real upstream record came back. (verified)
Why: npm undici@8 registers its global dispatcher under Symbol(undici.globalDispatcher.2), while Node 22's bundled fetch (undici 6.27) reads Symbol(undici.globalDispatcher.1). Two different globals. Your MockAgent โ and its disableNetConnect() safety net โ are simply not in the request path. The test passes against live data and nothing warns you. Only undici's own exported fetch (imported from the npm package) saw the mock in our runs; HttpService/axios sails past MockAgent in all cases, because axios in Node uses the http adapter, not undici.
This is version skew, not a permanent flaw โ with an npm undici that matches your Node's bundled major it behaves. But that's exactly the kind of thing that breaks on a Node upgrade with zero test failures to show for it. If you mock fetch in-process, prefer nock 14 and assert on the mocked body, not just the status.
Test.createTestingModule(...).overrideProvider(CatalogService).useValue(fake) is Nest's clean way to swap your own providers in unit tests โ verified it hands back the fake. But it's dependency injection rewiring inside the test process. It doesn't intercept HTTP; anything that still holds a URL still talks to the real host. Keep it for isolating your services; it is not an external-API mock.
We booted the actual Nest app (NestFactory.create, port 3111) whose controller proxies an upstream API through HttpService, registered a nock intercept for that upstream in the test process, then curled the running server:
// test process
nock('https://api.example.com').get('/products/1').reply(200, { name: 'NOCKED' });
const viaServer = await fetch('http://127.0.0.1:3111/catalog/1');
// โ NOT intercepted. The server fetched the real upstream. (verified)
Obvious in hindsight โ nock patches this process, the server is another one โ but it's exactly what happens when your e2e suite targets nest start, a Docker compose stack, or a deployed preview. In-process mocks also give nothing to a Playwright browser, a queue worker, a mobile app, or a teammate's curl. For those, the mock has to be a URL.
Mockbird is a free hosted mock API: one curl (or one click) creates a project with seeded, realistic records behind real CRUD endpoints โ REST + GraphQL, filtering, pagination, relations, CORS on. No signup needed.
curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H 'content-type: application/json' \
-d '{"name":"catalog-mock","preset":"ecommerce"}'
# โ { "id": "abc123xyz9", ... } your base URL: https://mockbird.mockbird.workers.dev/m/abc123xyz9
Your app code doesn't change โ inject the base URL the way Nest wants you to, via ConfigService:
// catalog.service.ts
constructor(private http: HttpService, config: ConfigService) {
this.base = config.get('CATALOG_API'); // prod URL in prod, Mockbird URL in test/e2e envs
}
product(id: number) {
return firstValueFrom(this.http.get(`${this.base}/products/${id}`));
}
Everything below ran against the shared demo project through HttpService itself, inside a compiled Nest testing module:
// forced failure โ your catchError/interceptor path, on any URL, no re-registration
GET {base}/products?mock_status=503
// โ AxiosError, response.status 503, body {"error":"simulated 503 error (mock_status)"} (verified)
// real latency โ your timeout config genuinely fires
GET {base}/products?mock_delay=3000 with { timeout: 1000 }
// โ throws code ECONNABORTED (verified)
// deterministic recovery for rxjs retry logic: 503, 503, then 200
http.get(`${base}/products/1?mock_seq=503,503,200&mock_seq_key=ci-run-1`).pipe(retry(3))
// โ recovers on the 3rd attempt, status 200, real record (verified)
And unlike a canned stub, the store is real โ writes persist:
POST {base}/orders { productId: 1, quantity: 2, status: "pending" } // โ 201, real id
GET {base}/orders/26 // โ the order, actually there
DELETE {base}/orders/26 โ GET again โ 404 // cleanup is real too (verified)
There's also ?mock_chaos=0.3 for random-failure soak tests, ?mock_jitter for latency spread, a mock JWT auth flow (real signed tokens, login/register/me) for testing auth interceptors, and named snapshots you can pin per-request with X-Mockbird-Snapshot so parallel Jest workers each see their own data state โ deterministic test data guide.
One Nest-12-specific footnote we hit while verifying: Nest 12 packages ship "type": "module" (ESM-only). If a quick Node script greets you with ERR_PACKAGE_PATH_NOT_EXPORTED or MODULE_NOT_FOUND on require, that's why โ use import in .mjs/ESM context.
| NestJS testing world | Mockbird | Notes |
|---|---|---|
nock(host).get(path).reply(...) | a resource on a real URL | list/get/create/update/delete generated, plus filtering, sorting, pagination, relations |
| Hand-rolled fixture objects | seeded realistic records | faker-style names/emails/prices/dates; or import your exact records from db.json/CSV/OpenAPI/Postman |
.reply(500) | ?mock_status=500 | on any URL, no re-registration (verified via HttpService) |
| Ordered replies for retry tests | ?mock_seq=503,503,200 | deterministic sequence, keyed per test run (verified with rxjs retry) |
| (no real latency) | ?mock_delay / ?mock_jitter | real time passes โ axios timeout genuinely fires (verified) |
| (no flakiness simulation) | ?mock_chaos=0.5 | real 5xx/429s for your retry/circuit-breaker logic |
overrideProvider | env-var base URL via ConfigService | keep overrides for swapping your providers; point the base URL at the mock for everything with a network hop |
nock.cleanAll() between tests | snapshot pinning / restore | named data states, safe across parallel workers |
scope.done() assertions | request inspector | last 50 requests with method, path, query, headers, body |
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?limit=2&select=name,price'
curl -i 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_status=503'
curl 'https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_delay=2000'
All against the shared demo project (resets daily).
overrideProvider for isolating your own DI graph. Point nest start, e2e suites, Playwright runs, workers and frontend fetches at a Mockbird base URL via config. App code doesn't change.GET /m/<project>/db.json ejects your entire dataset any time; openapi.json, types.ts and postman.json are generated from your live schema.nock / MockAgent / overrideProvider | Mockbird | |
|---|---|---|
| What it is | npm libraries + framework test features | free hosted service |
| Reachable from | the test process only | anything with HTTP: browser, Playwright, curl, SDKs, workers, mobile, CI, teammates |
A running nest start server | not covered (verified) | covered โ it's just a URL in config |
| Coverage of HTTP clients | nock 14: axios + fetch (verified); undici@8 MockAgent: missed both on Node 22 (verified) | every client by definition |
| Setup | register stubs per test | one curl or one click; no code |
| Stateful CRUD | no โ scripted answers only | default โ writes persist (verified) |
| Latency/retry/timeout testing | instant answers โ durations never run | ?mock_delay/?mock_seq/?mock_chaos, real time passes (verified) |
| Works offline | yes | no โ it's a real network call |
| Request assertions | scope.done(), precise, in-test | request inspector (last 50, headers/body) |
| Request cap | none | 10,000/project/day |
Written by the Mockbird maker โ bias disclosed. Where the in-process tools genuinely win: they run offline at zero latency, nock 14's coverage of both axios and fetch is real and its disableNetConnect() (when it's actually in the request path) turns unmocked calls into loud failures, and overrideProvider is the cleanest provider-swap mechanism in the Node ecosystem. For fast unit tests they should stay your default. When the thing you need is a URL โ for nest start, an e2e suite, a worker, a frontend fetch, Playwright, a teammate, or CI against a deployed preview โ that's us.
Full API reference in the docs. More guides: mock APIs in Express ยท mock API for Node.js ยท nock alternative ยท mock APIs in Jest ยท mock APIs in Playwright ยท deterministic test data ยท testing loading & error states. Create your API โ