1Node.js for SDET — Start Here Before Playwright
Read this before Playwright. Playwright runs on Node.js. Every
await in your tests, every worker process that runs parallel tests, every npm test command — all of it is Node.js. Without understanding Node's event loop, async model, and module system, Playwright's architecture won't make sense. This module gives you exactly what you need, no more, no less.What is Node.js? Node.js is a JavaScript runtime built on Chrome's V8 engine. Before Node, JavaScript could only run inside browsers. Node.js (released 2009) let JavaScript run on servers, build tools, CLI tools — and test runners. That's why Playwright, Jest, Vitest, and virtually all modern test frameworks are Node.js programs. When you run
npx playwright test, you're running a Node.js program that spawns browser processes and communicates with them over WebSocket.What you will master here
- Node runtime model: single thread + event loop + libuv
- Modules: CommonJS vs ESM, package.json fields
- npm / yarn / pnpm — lockfiles, scripts, workspaces
- Async patterns: callbacks → promises → async/await → streams
- Useful core modules: fs, path, http, child_process, crypto, util
- Express minimal example
- Worker Threads + cluster
- Debugging Node + memory
1.1 Runtime model
Node is JavaScript + libuv (C event loop). One main thread runs your JS; libuv handles I/O on a thread pool. Callbacks/promises return to the main thread when work completes.
main thread: parse JS → run sync code → await event loop event loop: timers → pending → poll (I/O) → check → close thread pool: fs reads, DNS, crypto (heavy)
1.2 Modules
// CommonJS (legacy default)
const fs = require('fs');
module.exports = { foo };
// ESM (modern)
import fs from 'node:fs/promises';
export { foo };
// Enable: package.json "type": "module" OR .mjs extension
// Dynamic import (works in both)
const mod = await import('./plugin.js');
1.3 package.json essentials
{
"name": "my-tests",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"test": "playwright test",
"lint": "eslint .",
"build": "tsc"
},
"dependencies": { "axios": "^1.7.0" },
"devDependencies": { "@playwright/test": "1.50.0", "vitest": "1.6.0" },
"engines": { "node": ">=20" }
}
Version specifiers
^1.7.0— compatible: 1.7.x, 1.8.x, … (up to next major)~1.7.0— patch only: 1.7.x1.7.0— exact*— any
1.4 npm vs yarn vs pnpm
| Tool | Pros | Cons |
|---|---|---|
| npm | Bundled with Node, mature | Slower than pnpm |
| yarn (classic) | Lockfile, workspaces | Mostly superseded |
| yarn berry (v2+) | PnP install, zero-installs | Compatibility gotchas |
| pnpm | Hard-links packages — fastest, smallest disk | Strict; sometimes catches sloppy deps |
1.5 Async patterns evolution
// 1. Callbacks (legacy)
fs.readFile('a.txt', 'utf8', (err, data) => {
if (err) return console.error(err);
console.log(data);
});
// 2. Promises (Node 10+)
fs.promises.readFile('a.txt', 'utf8').then(console.log).catch(console.error);
// 3. async/await (modern default)
async function read() {
try {
const data = await fs.promises.readFile('a.txt', 'utf8');
console.log(data);
} catch (e) { console.error(e); }
}
// 4. Streams (for large data)
const stream = fs.createReadStream('big.csv');
for await (const chunk of stream) process(chunk);
1.6 Useful core modules
// fs (filesystem)
import { readFile, writeFile, mkdir, readdir, stat } from 'node:fs/promises';
// path — cross-platform paths
import path from 'node:path';
path.join('a', 'b', 'c.txt'); // 'a/b/c.txt' or 'a\\b\\c.txt' on Windows
path.resolve(import.meta.dirname, 'data.json');
// http — built-in HTTP server / client
import http from 'node:http';
http.createServer((req, res) => res.end('ok')).listen(3000);
// child_process — run external commands
import { execSync, spawn } from 'node:child_process';
const out = execSync('git rev-parse HEAD').toString().trim();
const proc = spawn('npm', ['test'], { stdio: 'inherit' });
// crypto
import { randomUUID, createHash } from 'node:crypto';
const id = randomUUID();
const hash = createHash('sha256').update('x').digest('hex');
// util
import { promisify, inspect } from 'node:util';
const sleep = promisify(setTimeout);
await sleep(1000);
console.log(inspect(obj, { depth: 4, colors: true }));
1.7 Minimal Express server
import express from 'express';
const app = express();
app.use(express.json());
const users = [{ id: 1, name: 'Alice' }];
app.get('/users', (req, res) => res.json(users));
app.post('/users', (req, res) => {
const u = { id: Date.now(), ...req.body };
users.push(u);
res.status(201).json(u);
});
app.use((err, req, res, next) => res.status(500).json({ error: err.message }));
app.listen(3000, () => console.log('listening on 3000'));
1.8 Workers + cluster
- Worker Threads — true parallel JS on multiple cores. Use for CPU-heavy work (image processing, hashing).
- Cluster — fork the main process; each worker handles connections. Built into Node; PM2 / production frameworks use it under the hood.
// Worker
import { Worker } from 'node:worker_threads';
const w = new Worker('./hash-worker.js', { workerData: { input: 'big' }});
w.on('message', (hash) => console.log(hash));
1.9 Debugging + memory
node --inspect-brk app.js # opens chrome://inspect debugger node --max-old-space-size=4096 app.js # raise heap to 4GB # Heap snapshot kill -SIGUSR2 <pid> # writes heapdump # Open in Chrome DevTools → Memory → Load # Profile node --prof app.js node --prof-process isolate-0x*.log
Module 60 — Node.js Q&A
Is Node single-threaded?
Your JavaScript runs on one main thread. Heavy I/O (fs, crypto, DNS) runs on libuv's thread pool. For CPU-heavy work, use Worker Threads or fork multiple processes (cluster).
What is the event loop?
A loop that drains queued callbacks in phases (timers → pending → poll → check → close). Microtasks (Promise.then) run BETWEEN phases. Same concept as the browser's event loop (Module 18).
CommonJS vs ESM in Node?
CommonJS uses
require/module.exports, synchronous, legacy default. ESM uses import/export, asynchronous, modern. Enable ESM via "type":"module" in package.json or .mjs extension.npm ci vs npm install?
npm ci: deterministic — reads package-lock.json strictly, wipes node_modules, fails on lockfile drift. npm install: may update lockfile. Use npm ci in CI.
What's ^1.7.0 vs ~1.7.0?
^ allows minor + patch updates (1.7.x, 1.8.x, … <2.0.0). ~ allows patch only (1.7.x <1.8.0). Exact (1.7.0) pins. Use exact in lockfiles; ^ in package.json is conventional.
How do you run a long-running task without blocking?
For I/O-bound: async/await with the promise APIs. For CPU-bound: spawn a Worker Thread (in-process, message-based) or fork a child process. Don't run heavy sync code on the main thread — it stalls all requests.
What's Express middleware?
A function with signature
(req, res, next) => {} that runs before route handlers. Chain via app.use(...). Used for parsing JSON, auth, logging, error handling.How do you handle uncaught errors?
Wrap async handlers; use a global Express error middleware (4-arg signature). For top-level:
process.on('uncaughtException') + process.on('unhandledRejection') log and exit. Don't keep running after an unknown error state.How do you stream a large file response?
fs.createReadStream(path).pipe(res). Streams chunks without loading the whole file in memory. Backpressure handled by pipe.How do you diagnose a Node memory leak?
Trigger a heap dump (
kill -SIGUSR2 pid or v8.writeHeapSnapshot()), open in Chrome DevTools Memory tab, compare snapshots over time — growing constructors point at the leak. Common culprits: closures holding large refs, listeners not removed, caches without TTL.What's process.env and how do you load .env?
Environment variables exposed as a JS object. .env files need a loader:
import 'dotenv/config' (Node 20.6+ also has node --env-file=.env built-in).