Node.js for SDET — Start Here

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.4 npm vs yarn vs pnpm

ToolProsCons
npmBundled with Node, matureSlower than pnpm
yarn (classic)Lockfile, workspacesMostly superseded
yarn berry (v2+)PnP install, zero-installsCompatibility gotchas
pnpmHard-links packages — fastest, smallest diskStrict; 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
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).