Node.js 18: Native Fetch API and Experimental Test Runner
Node.js 18 dropped on April 19th, and for the first time in a while, I’m genuinely excited about a Node release. Not because of performance improvements or security patches — those are table stakes — but because of two features that address long-standing gaps in the platform: a global fetch API and a built-in test runner.
Both are experimental. Both have caveats. And both signal a direction for Node.js that I think is overdue.
Global Fetch: Finally #
Let me put this in perspective. The Fetch API has been available in browsers since 2015. Seven years. For seven years, if you wanted to make an HTTP request in Node.js with the same API you used in the browser, you had to npm install node-fetch or axios or one of a dozen other HTTP client libraries.
That era is over. Node.js 18 ships with a global fetch() enabled by default, powered by the Undici HTTP client. No import required. No package to install. Just call fetch() and it works.
const response = await fetch('https://api.github.com/users/hlibco');
const data = await response.json();
console.log(data.name);That’s it. No const fetch = require('node-fetch'). No import axios from 'axios'. Just the browser-standard Fetch API, available globally.
The implementation uses Undici under the hood, which is worth knowing because Undici is fast — significantly faster than the built-in http module for many workloads. It’s also HTTP/1.1-only for now (HTTP/2 support is coming), but for the vast majority of API consumption use cases, that’s fine.
A Few Caveats #
It’s marked as experimental, which means the API surface could change in future releases. In practice, I’d be surprised if the core fetch() signature changes — it follows the WHATWG Fetch Standard closely, and deviating from that would defeat the purpose.
What might change are the edge cases: how AbortController integrates, timeout behavior, custom agent configuration, and proxy support. If you’re building production infrastructure around fetch in Node 18 today, be prepared for some churn in those areas.
The other thing to know: node-fetch and axios aren’t going anywhere. They offer features that the native fetch doesn’t yet (interceptors in axios, custom agents in node-fetch) and they have years of battle-tested edge case handling. For simple HTTP requests, native fetch is the right choice going forward. For complex HTTP client needs, the ecosystem libraries still have their place.
Built-in Test Runner: The node:test Module
#
This one caught me off guard. Node.js 18 ships with a native test runner module accessible via import test from 'node:test'. After years of the Node ecosystem depending on Jest, Mocha, Vitest, Tape, and AVA — I’ve used all of them at various points — there’s now a built-in option.
import test from 'node:test';
import assert from 'node:assert';
test('addition works', (t) => {
assert.strictEqual(1 + 1, 2);
});
test('async operations', async (t) => {
const result = await Promise.resolve(42);
assert.strictEqual(result, 42);
});Run it with node --test your-test-file.mjs and you get TAP output. Subtests are supported. describe and it are available for BDD-style organization. beforeEach and afterEach hooks work as expected.
Should You Switch From Jest? #
No. Not yet. And maybe not ever, depending on your needs.
Jest (and Mocha, and the rest) have massive ecosystems: snapshot testing, mocking utilities, code coverage integration, watch mode, parallel execution with worker threads, custom reporters. The built-in test runner has none of that today. It’s deliberately minimal.
Where node:test makes sense right now:
- Library authors who want zero test dependencies. If you’re publishing an npm package and your tests only need basic assertions, shipping without a test framework dependency is appealing.
- Quick scripts and prototypes where spinning up a Jest config feels like overkill.
- CI pipelines for simple validation — health checks, smoke tests, contract tests.
Where it doesn’t make sense (yet): application testing at scale, projects that rely on Jest’s snapshot testing, anything that needs sophisticated mocking. The gap is real, and it’ll take time to close.
The interesting long-term question is whether node:test will evolve to be a serious Jest alternative or whether it’ll stay minimal by design. I suspect the Node.js team will keep it lean and let the ecosystem build on top of it, which is probably the right call.
V8 10.1: The Quiet Upgrades #
Node 18 ships with V8 10.1, and there are a few language features worth knowing about.
Array grouping methods — Array.prototype.group() and Array.prototype.groupToMap() — let you group array elements by a callback function. If you’ve ever written a reduce to group items by a property (and who hasn’t?), this is a welcome native alternative:
const inventory = [
{ name: 'asparagus', type: 'vegetables' },
{ name: 'bananas', type: 'fruit' },
{ name: 'cherries', type: 'fruit' },
];
const grouped = inventory.group(({ type }) => type);
// { vegetables: [...], fruit: [...] }
Class static initialization blocks are finally here. If you’ve been initializing static class members with awkward IIFE patterns or external setup functions, static {} blocks solve that cleanly.
Web Streams API on globals. ReadableStream, WritableStream, and TransformStream are now available as globals, aligning Node.js with the browser streaming API. This is particularly relevant if you’re working with streaming responses from fetch — the pieces fit together naturally.
LTS Timeline #
Node.js 18 enters Active LTS in October 2022 under the codename “Hydrogen.” It’ll be actively maintained until April 2025. If you’re on Node 16 LTS, you don’t need to rush — Node 16 is supported until September 2023. But I’d start testing your applications against Node 18 now, especially if you want to adopt native fetch or the test runner.
For greenfield projects starting today? Just use Node 18. The experimental flags on fetch and node:test are more about API stability guarantees than actual reliability concerns. Both work well enough for development, and they’ll be stable long before you ship to production.
The Direction Matters More Than the Features #
What excites me about this release isn’t fetch or node:test individually. It’s the direction they represent. Node.js is converging with web platform APIs in a way it hasn’t before. Global fetch, web streams, structured clone — the gap between browser JavaScript and server JavaScript is narrowing.
For developers who work across the stack (which, let’s be honest, is most of us), this convergence matters. Less context switching, fewer “how do I do this in Node again?” moments, more code that works in both environments without polyfills.
Node 18 isn’t revolutionary. But it’s a release that makes the platform feel like it’s evolving in the right direction. And sometimes that’s more valuable than any single feature.