Your First Script
Writing your first useful Node script: shebang, parsing args, reading files, writing output, ESM and TypeScript options.
Node — first script
EXAMPLE
// ===== A minimal script (CommonJS) =====
// hello.js
console.log('hello,', process.argv[2] ?? 'world');
node hello.js Alex
// hello, Alex
// ===== ESM (the default in 2026) =====
// package.json: { "type": "module" }
// hello.mjs (or .js with type:module)
import { readFile } from 'node:fs/promises';
const text = await readFile('package.json', 'utf8');
console.log('size:', text.length);
// ===== Top-level await + parseArgs =====
// list.js
import { parseArgs } from 'node:util';
import { readdir } from 'node:fs/promises';
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
dir: { type: 'string', short: 'd', default: '.' },
long: { type: 'boolean', short: 'l' },
},
allowPositionals: true,
});
const entries = await readdir(values.dir, { withFileTypes: true });
for (const e of entries) {
console.log(values.long ? \`${e.isDirectory() ? 'd' : 'f'} ${e.name}\` : e.name);
}
// node list.js -d src -l
// ===== Shebang (run without 'node' prefix) =====
// At the top of the file:
#!/usr/bin/env node
// Make executable:
chmod +x list.js
./list.js -d src
// ===== Read stdin =====
// upper.js — reads stdin, upper-cases, writes stdout
import { stdin, stdout } from 'node:process';
let data = '';
for await (const chunk of stdin) data += chunk;
stdout.write(data.toUpperCase());
// echo 'hello' | node upper.js
// ===== Fetch (built-in since 18) =====
const res = await fetch('https://api.github.com/repos/nodejs/node');
const json = await res.json();
console.log(json.stargazers_count);
// ===== Spawn a child process =====
import { spawn } from 'node:child_process';
const git = spawn('git', ['status', '--short']);
git.stdout.pipe(process.stdout);
git.on('close', (code) => console.log('exit', code));
// ===== TypeScript option =====
// tsx (recommended) — run TS directly:
npm i -D tsx
npx tsx list.ts
// Node 22+ has a flag-gated --experimental-strip-types to run plain TS too.
// ===== Patterns to internalise =====
// - parseArgs over yargs / commander for small scripts
// - Top-level await with ESM keeps scripts flat
// - process.exit(1) on failures so shells see the error
// - Streams for big data; never read 10GB files into memory
// ===== Pitfalls =====
// - Mixing require + import in one file
// - Forgetting 'await' on async calls -> empty output
// - No error handling -> unhandled rejection warnings
// - Writing to stdout from logs -> mix with piped output; use stderr
Why it matters
A useful script is parseArgs + a fetch or fs call + a clear exit code. ESM + top-level await keeps the code flat; tsx adds TypeScript with zero ceremony. Scripts under 100 lines are where Node truly shines; promote to a real project only when complexity demands.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…