Skip to content

Core APIs

Node.js ships with a rich set of built-in globals and modules that are available without installing anything. These are the primitives every Node.js program is built on.

Key globals and modules covered in this module:

  • process — access to the running process: environment variables, command-line arguments, the working directory, the platform, and lifecycle control.
  • Buffer — a fixed-length chunk of raw binary data. Predates typed arrays and remains the standard way to work with bytes in Node streams and networking.
  • node:fs / node:fs/promises — read, write, and navigate the filesystem. The promises API lets you use async/await natively.
  • EventEmitter — the backbone of Node’s event-driven architecture. Streams, the HTTP server, and many third-party libraries all extend EventEmitter.
LessonTopic
process & envprocess.argv, process.env, exit codes, and platform detection
BufferBinary data, encodings, and byte-level manipulation
File Systemfs callbacks, sync APIs, and the fs/promises interface
EventEmitterCustom events, listeners, and the once / removeListener lifecycle

Node keeps absorbing popular packages into the runtime:

  • node:sqlite — an embedded SQLite database (unflagged since 22.13, still Experimental on Node 24 — importing it prints an ExperimentalWarning). Zero dependencies for local state, tests, and CLIs.
  • fs.glob (22+) — file matching without the glob package.
  • node:test — the test runner covered in the last module.
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT) STRICT');
const insert = db.prepare('INSERT INTO notes (body) VALUES (?)');
insert.run('first');
insert.run('second');
console.log(db.prepare('SELECT * FROM notes ORDER BY id').all());
// [ { id: 1, body: 'first' }, { id: 2, body: 'second' } ]

The snippet below reads two globals that are available in every Node.js process without any require or import:

Node.js
Output (verified on Node 24)
Node version: v24.13.0
String: Hello, Node!
Chars: 12
Bytes (UTF-8): 12

Two of these are true globals reachable from anywhere with zero import; the other two are modules you must pull in explicitly — and one of those modules (EventEmitter) is the base class the other three quietly depend on.

flowchart TB
  subgraph Globals["always available, no import"]
    process["process"]
    Buffer["Buffer"]
  end
  subgraph Modules["explicit import"]
    fs["node:fs / node:fs/promises"]
    events["node:events — EventEmitter"]
  end
  events -->|"extended by"| fsStream["fs.ReadStream / fs.WriteStream"]
  fs -->|"returns"| fsStream
  events -->|"extended by"| httpNet["http.Server, net.Socket (later modules)"]
  process -->|"reports on"| httpNet
process and Buffer need no import; fs and EventEmitter do — and EventEmitter is the base class fs streams and later HTTP/net objects extend
ChoiceBenefitCost
process / Buffer as globalsZero-friction access from any file, no import churnHarder to see a dependency at a glance — a function using process.env gives no import-level hint that it reads config
Optional node: prefix on legacy modules (fs, path)Old code (require('fs')) keeps working foreverTwo spellings of the same module coexist in a codebase — inconsistent style
Mandatory node: prefix on new modules (node:test, node:sqlite)Impossible to shadow with an npm package named test or sqliteNo unprefixed fallback — copy-pasting old-style require('sqlite') fails with MODULE_NOT_FOUND instead of a clearer error
  • “Every Node built-in needs an import” — fact: process and Buffer are globals; only modules like node:fs and node:events need import/require.
  • “The node: prefix is just decoration” — fact: for legacy modules it’s optional style; for node:test and node:sqlite it is the only valid specifier — there is no bare require('test').
  • “node:sqlite is production-ready because it ships in Node 24” — fact: it still prints an ExperimentalWarning on every import on 24.13; the API can change in a semver-minor release.
  • Does generated code require('sqlite') or require('test') without the node: prefix? Both throw MODULE_NOT_FOUND — these two modules have no unprefixed form, unlike fs or path.
  • Does a snippet treat node:sqlite as stable (no warning handling, used casually in examples) when it is still Experimental on the target Node version?
  • Does the code import Buffer or process from somewhere (import { Buffer } from 'node:buffer')? That’s valid but unnecessary — double-check it isn’t hiding a typo like bufer that a linter would have caught on the global form.

🐛 Spot the bug

const { test } = require('test');
test('adds numbers', () => {
console.log('ok');
});
Show the answer

node:test has no unprefixed form — only require('node:test') (or require('test') under --experimental-require-module… no, that flag doesn’t exist either) resolves. Plain require('test') throws Error [ERR_MODULE_NOT_FOUND] / MODULE_NOT_FOUND: Cannot find module 'test', because Node does not register bare test as a module specifier — only node:test. The fix is require('node:test') (or import { test } from 'node:test'). This is the same rule as node:sqlite: brand-new built-ins only exist under the prefixed name, unlike legacy modules such as fs where both spellings work.

Which of the following is a true Node.js global — available without any import?
What is the recommended modern prefix for importing a Node.js built-in module?
What does require('test') throw on Node 24, given node:test is the built-in test runner?
What happens the first time a process imports node:sqlite on Node 24.13?
A teammate writes const Buffer = require("node:buffer").Buffer; at the top of every file. Is this necessary?