The Node.js APIs you reach for every day — and the ones you look up every time. This covers npm, the file system, HTTP, streams, events, process management, and the async patterns that make Node tick.
Quick reference
The 25 patterns that cover 90% of daily Node work.
| Pattern | Code |
|---|---|
| Read file (sync) | fs.readFileSync('f.txt', 'utf8') |
| Read file (async) | await fs.promises.readFile('f.txt', 'utf8') |
| Write file | await fs.promises.writeFile('f.txt', data) |
| List directory | await fs.promises.readdir('./src') |
| Join paths | path.join(__dirname, 'data', 'file.json') |
| Env variable | process.env.PORT ?? '3000' |
| CLI args | process.argv.slice(2) |
| HTTP server | http.createServer((req, res) => ...) |
| Parse JSON body | JSON.parse(body) after collecting chunks |
| Spawn process | spawn('git', ['log', '--oneline']) |
| EventEmitter | emitter.on('event', handler) |
| Read stream | fs.createReadStream('big.csv') |
| Write stream | fs.createWriteStream('out.txt') |
| Pipe streams | readable.pipe(writable) |
| Hash string | crypto.createHash('sha256').update(s).digest('hex') |
| UUID | crypto.randomUUID() |
| Random bytes | crypto.randomBytes(32).toString('hex') |
| Resolve module | require.resolve('express') |
| Current file | import.meta.url (ESM) / __filename (CJS) |
| Current dir | import.meta.dirname (ESM) / __dirname (CJS) |
| Exit with code | process.exit(1) |
| Graceful shutdown | process.on('SIGTERM', cleanup) |
| Unhandled rejection | process.on('unhandledRejection', handler) |
| Timer | const id = setTimeout(fn, 1000); clearTimeout(id) |
| Immediate | setImmediate(fn) — after I/O, before timers |
npm / npx commands
# Package management
npm init -y # create package.json (defaults)
npm install express # install and save to dependencies
npm install -D typescript # save to devDependencies
npm install -g nodemon # global install
npm uninstall lodash # remove package
npm update # update within semver ranges
npm outdated # list outdated packages
npm list --depth=0 # top-level installed packages
npm ci # clean install from package-lock.json (CI)
# Running scripts
npm run build
npm run dev
npm test # shorthand for npm run test
npm start # shorthand for npm run start
# npx — run without installing globally
npx create-next-app my-app
npx tsc --noEmit # run TypeScript compiler once
npx prettier --write .
# Workspaces
npm install --workspace=packages/api express
npm run build --workspaces
# Semver prefixes in package.json
# "^1.2.3" → compatible: 1.x.x (minor + patch)
# "~1.2.3" → patch only: 1.2.x
# "1.2.3" → exact version
# "*" → latest (dangerous)
Module system
// CommonJS (CJS) — default in .js files without "type": "module"
const fs = require('fs');
const { join } = require('path');
const myModule = require('./my-module'); // local: must start with ./
module.exports = { myFunction };
module.exports = myFunction; // default export
// ES Modules (ESM) — "type": "module" in package.json, or .mjs files
import fs from 'fs';
import { join } from 'path';
import myModule from './my-module.js'; // extension required in ESM
export function myFunction() {}
export default myFunction;
// Dynamic import (works in both CJS and ESM)
const { default: chalk } = await import('chalk');
// __dirname / __filename in ESM (they don't exist natively)
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Or use Node 22+:
const __dirname = import.meta.dirname;
File system (fs)
Always prefer fs.promises (async) over the sync API in production code.
import { promises as fs } from 'fs';
import path from 'path';
// Read
const text = await fs.readFile('readme.txt', 'utf8');
const buf = await fs.readFile('image.png'); // Buffer
// Write
await fs.writeFile('output.txt', 'hello\n');
await fs.writeFile('data.json', JSON.stringify(obj, null, 2));
// Append
await fs.appendFile('log.txt', `${new Date().toISOString()} event\n`);
// Directory
await fs.mkdir('dist/assets', { recursive: true }); // like mkdir -p
const entries = await fs.readdir('./src');
const stats = await fs.stat('file.txt');
console.log(stats.isFile(), stats.size, stats.mtime);
// Copy / rename / delete
await fs.copyFile('src.txt', 'dst.txt');
await fs.rename('old.txt', 'new.txt');
await fs.unlink('temp.txt');
await fs.rm('dist/', { recursive: true, force: true }); // rm -rf
// Check existence (no throws)
const exists = await fs.access('file.txt').then(() => true).catch(() => false);
// Read directory recursively (Node 18.17+)
for await (const entry of fs.glob('src/**/*.ts')) {
console.log(entry);
}
// Or portable alternative:
const walk = async (dir) => {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = await Promise.all(entries.map(e =>
e.isDirectory() ? walk(path.join(dir, e.name)) : path.join(dir, e.name)
));
return files.flat();
};
Path module
import path from 'path';
path.join('/foo', 'bar', 'baz.txt') // /foo/bar/baz.txt
path.resolve('src', 'index.ts') // absolute from cwd
path.dirname('/foo/bar/baz.txt') // /foo/bar
path.basename('/foo/bar/baz.txt') // baz.txt
path.basename('/foo/bar/baz.txt', '.txt') // baz
path.extname('README.md') // .md
path.parse('/foo/bar.txt')
// { root: '/', dir: '/foo', base: 'bar.txt', ext: '.txt', name: 'bar' }
// Always use path.join, never string concatenation
// BAD: __dirname + '/data/' + file
// GOOD: path.join(__dirname, 'data', file)
HTTP server
import http from 'http';
import { URL } from 'url';
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
if (req.method === 'GET' && url.pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World');
return;
}
if (req.method === 'POST' && url.pathname === '/echo') {
// Collect body
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const body = JSON.parse(Buffer.concat(chunks).toString());
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ received: body }));
return;
}
res.writeHead(404);
res.end('Not found');
});
server.listen(3000, () => console.log('Listening on :3000'));
Streams
Node streams are the right tool for large files — they process data in chunks without loading everything into memory.
import fs from 'fs';
import { pipeline } from 'stream/promises';
import { createGzip } from 'zlib';
// Read → transform → write (don't use .pipe() — use pipeline)
await pipeline(
fs.createReadStream('big.log'),
createGzip(),
fs.createWriteStream('big.log.gz')
);
// Collect a stream into a Buffer
async function streamToBuffer(readable) {
const chunks = [];
for await (const chunk of readable) chunks.push(chunk);
return Buffer.concat(chunks);
}
// Transform stream (uppercase each line)
import { Transform } from 'stream';
const upper = new Transform({
transform(chunk, _enc, done) {
done(null, chunk.toString().toUpperCase());
}
});
await pipeline(
fs.createReadStream('input.txt'),
upper,
fs.createWriteStream('output.txt')
);
Events (EventEmitter)
import { EventEmitter } from 'events';
class Queue extends EventEmitter {
#items = [];
enqueue(item) {
this.#items.push(item);
this.emit('item', item);
}
dequeue() {
const item = this.#items.shift();
if (this.#items.length === 0) this.emit('empty');
return item;
}
}
const q = new Queue();
q.on('item', (x) => console.log('got:', x));
q.once('empty', () => console.log('queue drained'));
q.enqueue('a');
q.enqueue('b');
q.dequeue(); q.dequeue(); // → "queue drained"
// Async event handlers — use emitAsync pattern or wrapping
q.on('item', async (x) => {
await processItem(x); // errors here are swallowed! use try/catch inside
});
Process
// Environment variables
const port = process.env.PORT ?? '3000';
const isDev = process.env.NODE_ENV === 'development';
// CLI arguments — process.argv[0] = 'node', [1] = script path
const args = process.argv.slice(2);
// node script.js --output=dist --verbose
// → ['--output=dist', '--verbose']
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down...');
await server.close();
await db.disconnect();
process.exit(0);
});
process.on('SIGINT', () => process.emit('SIGTERM'));
// Uncaught errors — log and exit, never swallow
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
process.exit(1);
});
// Working directory
console.log(process.cwd()); // current working dir (where node was run)
process.chdir('/tmp'); // change cwd
// Memory usage
const mem = process.memoryUsage();
console.log(`RSS: ${Math.round(mem.rss / 1024 / 1024)} MB`);
Child processes
import { spawn, exec, execFile } from 'child_process';
import { promisify } from 'util';
// spawn — streaming, good for long-running processes
const git = spawn('git', ['log', '--oneline', '-10']);
git.stdout.on('data', (d) => process.stdout.write(d));
git.on('close', (code) => console.log('Exit:', code));
// exec — simple, buffers all output (max 1MB default)
const execAsync = promisify(exec);
const { stdout } = await execAsync('git rev-parse HEAD');
console.log('Commit:', stdout.trim());
// NEVER pass user input to exec/execFile unsanitized → command injection
// GOOD: use spawn with args array instead
spawn('git', ['log', userBranch]); // safe, no shell interpretation
// BAD: exec(`git log ${userBranch}`) // userBranch = "; rm -rf /"
Crypto
import crypto from 'crypto';
// UUID (built-in since Node 14.17)
const id = crypto.randomUUID(); // '550e8400-e29b-41d4-a716-446655440000'
// Hash
const hash = crypto.createHash('sha256').update('hello').digest('hex');
// HMAC (for signatures)
const sig = crypto.createHmac('sha256', secretKey).update(payload).digest('hex');
// Random bytes (for tokens, session ids)
const token = crypto.randomBytes(32).toString('hex'); // 64-char hex string
const b64 = crypto.randomBytes(24).toString('base64url');
// Timing-safe compare (for password/token comparison)
const ok = crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
6 common mistakes
1. Using __dirname in ES modules
__dirname and __filename don't exist in ESM. Use fileURLToPath(import.meta.url) or the newer import.meta.dirname (Node 22+).
2. Forgetting { recursive: true } on fs.mkdir
Without it, creating nested directories throws if any parent is missing. Always pass { recursive: true } unless you deliberately want the error.
3. Unhandled promise rejections crashing silently
Register process.on('unhandledRejection', ...) in your entry file. In Node 15+, unhandled rejections crash the process by default — which is correct. Never swallow them with an empty .catch(() => {}).
4. Using .pipe() instead of pipeline()
readable.pipe(writable) doesn't propagate errors — if readable errors, writable is not closed. Use pipeline() from stream/promises which handles cleanup automatically.
5. Passing user input to exec / execFile
These use a shell under the hood. exec('ls ' + userInput) is a command injection vulnerability. Always use spawn with an args array for anything containing user data.
6. process.cwd() vs __dirname
process.cwd() is where you ran node from — it changes based on the shell. __dirname is always the directory containing the current file. Use __dirname (or its ESM equivalent) for paths relative to your source files.
FAQ
What's the difference between require and import?
require is CommonJS (synchronous, dynamic). import is ES module (statically analysed, tree-shakeable). Add "type": "module" to package.json to use import in .js files; otherwise use .mjs. Most modern packages support both via exports in their package.json.
When should I use fs.readFileSync vs fs.promises.readFile?
Use the async version in servers and anywhere performance matters — sync I/O blocks the entire event loop. Sync is acceptable in CLI startup code or top-level scripts where blocking is fine and simplicity matters.
What's setImmediate vs setTimeout(fn, 0)?
setImmediate runs after the current event loop iteration completes I/O callbacks. setTimeout(fn, 0) runs in the timers phase, which comes before setImmediate in most cases — but the actual order depends on context. For "after current tick but asap", prefer setImmediate. For "definitely next tick", use process.nextTick (it runs before any I/O).
How do I read environment variables safely?
Use a library like dotenv to load .env in development, and validate at startup:
import 'dotenv/config';
const DB_URL = process.env.DATABASE_URL;
if (!DB_URL) throw new Error('DATABASE_URL is required');
Never commit .env files — add them to .gitignore.
How do I run code after all event loop tasks complete?
There's no built-in "event loop drain" hook. The cleanest pattern is to call server.close() then process.exit() in a SIGTERM handler after awaiting all in-flight requests.
What's the best way to handle errors in async Express routes?
Wrap handlers with a utility that forwards errors to next():
const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get('/user/:id', asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
res.json(user);
}));
app.use((err, req, res, _next) => {
res.status(err.status ?? 500).json({ error: err.message });
});
In Express 5+, async errors are forwarded automatically without the wrapper.