Node ships with dozens of modules that need no npm install at all - fs, http, path, crypto, and more are compiled directly into the node binary.
Collectively, that's Node's standard library, and it's large enough that a working backend service can often go a long way before reaching for its first third-party dependency.
Built-in APIs Basics covers the daily-driver modules with working code; this page is the map above that - what actually distinguishes a "built-in" from an npm package, how Node organizes and stabilizes these APIs over time, and why an increasing share of them now mirror the browser's Web Platform APIs instead of inventing Node-only shapes.
Built-in modules are code compiled directly into the Node binary and resolved through an internal registry, not through disk-based node_modules lookup - and Node's documentation tags each one with a Stability Index level that tells you how safe it is to depend on.
Insight: Knowing whether an API is a stable Node-native module, an adopted Web Platform API, or an experimental addition changes how confidently you can build on it and how it will behave when you upgrade Node.
When to Use This Model: Deciding whether a need is already covered by Node itself before reaching for npm, reasoning about whether an API is safe to rely on long-term, debugging TypeScript errors around builtin types, and reading fs, crypto, path, os/process, timers, and util pages with the right frame in place.
Limitations/Trade-offs: Built-ins version with the Node runtime itself, not independently - upgrading Node can change builtin behavior even when your package.json dependencies haven't moved at all.
Related Topics: module resolution, node: module specifiers, TypeScript in Node, the event loop.
A built-in module is source code that ships compiled inside the node executable rather than as a file on disk that has to be located and loaded.
That's the core distinction from anything on npm: requesting fs or crypto never touches the filesystem's node_modules resolution walk at all - Node recognizes the name and hands back functionality from its own internal registry, instantly.
Node adopted the explicit node: prefix (import fs from 'node:fs') to remove any ambiguity between a builtin and a same-named package someone could theoretically publish to npm.
The prefix is optional for the long list of original core modules for backward compatibility, but it's required for newer additions like node:test and node:sea, and it's the pattern current guidance recommends across the board because it makes a module's origin unambiguous at a glance.
A useful analogy: built-in modules are like a city's public utilities - water, power, roads.
They're always present, standardized, and you don't "install" them the way you'd hire a private contractor (an npm package); they just come with living in the city, and the city itself (the Node release) decides when and how they change.
import { readFile } from 'node:fs/promises'; // Node-native: callback-era API, promisifiedimport { randomUUID } from 'node:crypto'; // Node-native: server-focused crypto helpersconst res = await fetch('https://example.com'); // Web Platform API: same shape as the browser
Every documented Node API carries a Stability Index rating that governs how much change you should expect: Experimental APIs can change or disappear between minor versions, Stable APIs only break on a semver-major Node release, and Legacy APIs are maintained but actively discouraged in favor of a newer replacement.
Built-in APIs Basics covers how to actually read that marker in the docs; the mental model here is what it implies for your code - an Experimental API in production is a deliberate risk, not an oversight.
Loading mechanics differ fundamentally from npm packages: a builtin specifier resolves through an internal binding lookup baked into the runtime, so there's no disk I/O, no package.json main-field resolution, and no dependency on your project's module system settings beyond whether you're using CJS or ESM syntax at all.
That's also why builtins are versioned with the Node binary itself rather than independently - a feature like util.parseEnv or the global fetch only exists once you're on a Node version that shipped it, regardless of what's pinned in package.json.
The standard library actually splits into two distinguishable families that behave differently in practice.
Node-native APIs - fs, http, net, cluster - were designed specifically for Node's server-side, callback-and-EventEmitter-driven world, often modeled on POSIX system calls, and have no equivalent in a browser.
Adopted Web Platform APIs - fetch, URL, URLSearchParams, structuredClone, AbortController, and crypto.webcrypto - are Node's implementation of the same interfaces browsers expose, deliberately shaped to match so the same code can run in Node, a browser, or another JS runtime like Deno or Bun without a compatibility shim.
That convergence is a real strategic shift, not a cosmetic one: earlier Node versions solved every problem with a Node-specific API (http.get for network calls), while recent versions increasingly prefer shipping the Web-standard equivalent (fetch) alongside or instead of inventing something new.
The practical effect is that isomorphic packages - libraries meant to run in both Node and the browser - can rely on more of the platform directly, without bundling a polyfill for things Node now provides natively.
Node's own deprecation process is a first-class part of this model: deprecated APIs emit runtime warnings (visible via --pending-deprecation for APIs not yet warning by default), giving teams a window to migrate before an API is actually removed in a future major version.
That process is part of what "Stable" is supposed to mean - even Stable APIs can eventually be deprecated, but only through a documented, versioned path rather than a silent breaking change.
The security model differs meaningfully from npm dependencies as well.
Built-in modules ship through Node's own release and security-advisory process, with no registry, no package name to typosquat, and no transitive dependency tree to audit - which is a real reason to prefer a builtin over an equivalent npm package when one exists, beyond just avoiding an install.
That doesn't make builtins risk-free (Node itself gets CVEs, and misuse of a builtin like child_process is still a common vulnerability class) - it just moves the trust boundary from "thousands of maintainers on a public registry" to "the Node core team."
Observability and diagnostics are an underused corner of the standard library worth naming here: modules like node:perf_hooks, node:diagnostics_channel, and node:async_hooks exist specifically to instrument production Node processes without adding a tracing dependency, and frameworks like Express and Fastify, along with most ORMs and database drivers, are themselves built directly on top of node:http, node:net, and node:tls.
TypeScript is a special case worth calling out: because builtins are compiled C++ bindings exposed as JavaScript objects, they carry no type information of their own the way a hand-written .ts file would.
The @types/node package is a separately maintained set of hand-written declaration files that describes every builtin's shape - which is why it's a devDependency on virtually every Node TypeScript project, and why its version should track the Node version you actually target.
Source
Strength
Weakness
Best Fit
Node-native builtin (fs, http, crypto)
No install, no supply-chain risk, versions with the runtime
Node-specific API shape, sometimes callback-era ergonomics
Server-side I/O, OS-level and process-level needs
Adopted Web Platform API (fetch, URL)
Same shape as the browser, portable across runtimes
"If it's not on npm, I have to write it myself." A surprising amount of common need is already covered by builtins - UUID generation, HTTP clients, env parsing, and terminal styling all ship in modern Node without installing anything.
"The node: prefix is required for every core module." It's optional for the long-established modules (fs, path, http) for backward compatibility, but required for newer ones like node:test and node:sea.
"Built-in modules never change or break." Every builtin carries a Stability Index, and Experimental or Legacy-tagged APIs can change or be removed on a timeline much shorter than "never."
"Web Platform APIs behave identically in Node and the browser." The shape matches deliberately, but the semantics can differ - Node's fetch has no CORS enforcement and different default agent/proxy behavior than a browser's.
"TypeScript understands Node builtins out of the box." It doesn't - builtins are compiled bindings with no inherent type information, which is exactly what @types/node exists to supply.
What makes a module "built-in" instead of something you install?
It's compiled directly into the Node binary and resolved through an internal registry rather than a node_modules lookup on disk - no install step, no network fetch, no version to pin independently of Node itself.
Do I need the `node:` prefix on every core module import?
No - it's optional on long-established modules like fs and path for backward compatibility, but the newest additions (node:test, node:sea) require it, and using it consistently is the current recommended practice either way.
What does the Stability Index actually tell me?
It tells you how much change to expect: Experimental APIs can shift between minor Node versions, Stable APIs only break on a major version bump, and Legacy APIs are maintained but actively discouraged in favor of something newer.
Why does Node have both `node:http` and the global `fetch`?
node:http is Node's original, server-focused networking primitive; fetch is a later addition that mirrors the browser's API specifically so the same client code can run unmodified across Node, browsers, and other JS runtimes.
Are builtins versioned separately from the packages in my `package.json`?
No - a builtin's available features and behavior are tied entirely to the Node version you're running, which is why upgrading Node can change what's available even if no package.json dependency changed at all.
Why do I need `@types/node` if TypeScript already understands JavaScript?
Builtins are compiled C++ bindings exposed as plain JS objects, so they carry no built-in type information the way hand-written .ts source would - @types/node is a maintained set of declaration files describing their shapes.
Is it safer to use a builtin than an equivalent npm package?
Generally yes from a supply-chain angle - builtins ship through Node's own core release and security process, with no registry, no typosquatting risk, and no transitive dependency tree to audit, though builtins themselves can still have their own CVEs.
Does Node's global `fetch` behave exactly like a browser's?
It matches the same API shape deliberately, but the runtime environment differs - server-side fetch has no CORS enforcement and different default networking configuration (proxies, agents) than a browser context.
How does Node warn me before removing a deprecated API?
Deprecated APIs emit runtime deprecation warnings, and the --pending-deprecation flag surfaces warnings for APIs not yet warning by default - giving teams a documented window to migrate before an eventual removal.
Why are frameworks like Express built on top of `node:http` instead of replacing it?
node:http already handles the low-level request/response lifecycle and socket management; frameworks add routing, middleware, and ergonomics on top rather than reimplementing networking primitives Node already provides.
What's an example of a Node-specific need with no Web Platform equivalent?
Process and OS-level concerns - spawning child processes, forking for multiple CPU cores, reading host memory and CPU info - have no browser analog at all, since browsers have no concept of an operating system process to expose.