Node.js Scripts Basics
11 examples to get you started with Node.js Scripts -- 8 basic and 3 intermediate.
Prerequisites
You need Node.js 22 LTS or newer. Check with node --version; install via nvm, fnm, or Volta if you do not have it.
# Install or switch to the current LTS with nvm
nvm install --lts
nvm use --lts
node --version # should print v22.x or higherConventions for every example below:
- Files live in a folder with a
package.json. Runnpm init -yonce to create one. - Sources use ES Modules (
import/export). Set"type": "module"inpackage.json. - TypeScript examples run with
tsx-- no compile step needed during development.
Basic Examples
1. Verify Node and npm Are Installed
A one-liner to confirm your toolchain before you write any scripts.
node --version # v22.15.0 or similar
npm --version # 10.x or similar
node -e "console.log('hello from node', process.version)"node -e "<code>"runs a snippet inline -- perfect for smoke tests without creating a file.process.versionreports the running Node version so you catch wrong-version issues immediately (e.g., old PATH picking up an old binary).- If
nodeis missing, install vianvm(macOS/Linux) orfnm/Volta(cross-platform) -- avoid the global Homebrew install on shared machines. - Pin the version with a
.nvmrc(or.node-version) file so every contributor runs the same runtime.
Related: Installing Node.js and npm -- nvm, fnm, Volta, and the LTS cadence
2. Hello World JavaScript Script
The smallest useful .js file that you can run with node.
// hello.js
const greeting = "Hello, Node.js!";
console.log(greeting);Run it:
node hello.js- Any
.jsfile with valid code runs directly undernode-- no bundler, no config. - With
"type": "module"inpackage.json,.jsfiles are treated as ES Modules (import/export). Without it, they are CommonJS (require/module.exports). - Use
node --watch hello.jsduring development to re-run on save (Node 18+ built-in). process.argvholds CLI arguments;process.envholds env vars -- both are always available.
Related: Running JavaScript scripts -- entry points, watch mode, shebang lines | ESM vs CommonJS -- picking the module system
3. Run a TypeScript Script with tsx
Skip the compile step in development -- tsx runs .ts files directly.
npm install --save-dev tsx typescript @types/node// sum.ts
function sum(a: number, b: number): number {
return a + b;
}
console.log(sum(2, 3));Run it:
npx tsx sum.tstsxuses esbuild under the hood -- fast startup and the same type-stripping behavior as production builds.- Use
tsx watch sum.tsfor a live-reload dev loop; usenpx tsconly when you want to produce.jsoutput for deployment. - Add
@types/nodeso built-ins likefs,path, andprocessare typed. - Node 22.6+ ships native type stripping (
node --experimental-strip-types sum.ts) -- a good fallback when you cannot addtsx.
Related: Running TypeScript Scripts -- tsx vs ts-node vs native stripping | ESLint for Node.js Scripts -- wiring lint to a TS script project
4. Install Packages with npm or pnpm
Both tools use the same package.json; pnpm is faster and disk-efficient.
# npm
npm install zod
npm install --save-dev tsx
# pnpm (enable via Corepack, no global install needed)
corepack enable
pnpm add zod
pnpm add -D tsxpackage.jsonis the source of truth -- both tools read/write it. Switching between them is safe if your team agrees on one at a time.- pnpm's content-addressable store dedupes copies across projects -- big disk and install-time wins on machines with many repos.
- Commit exactly one lockfile:
package-lock.jsonfor npm,pnpm-lock.yamlfor pnpm. Delete the other. - Use
corepackto pin the exact package manager version inpackageManagerso CI and laptops stay aligned.
Related: pnpm vs npm -- workspaces, hoisting, and performance comparison
5. ESM Imports
Use the modern module system -- static imports, top-level await, named exports.
// package.json
{
"type": "module"
}// math.ts
export function sum(a: number, b: number) {
return a + b;
}// app.ts
import { sum } from "./math.js";
console.log(sum(2, 3));- Set
"type": "module"inpackage.jsonso.js/.tsfiles are ESM by default. - ESM requires explicit file extensions in imports --
./math.jseven when the source ismath.ts. - Top-level
awaitis allowed in ESM:const data = await fetch(...)at the top of a file works. - Mixing CJS and ESM in the same file is painful -- pick one and stick with it for the project.
Related: ESM vs CommonJS -- interop rules,
createRequire, and when CJS still makes sense
6. Read a File with fs/promises
Read and write files asynchronously -- the recommended API since Node 14.
// read-config.ts
import { readFile, writeFile } from "node:fs/promises";
const text = await readFile("config.json", "utf8");
const config = JSON.parse(text) as { apiUrl: string };
config.apiUrl = config.apiUrl.replace("http:", "https:");
await writeFile("config.json", JSON.stringify(config, null, 2));fs/promisesreturns Promises -- pair with top-levelawaitfor clean scripts.- Always pass the encoding (
"utf8"); omit it only if you want aBuffer. - Prefix built-in modules with
node:(node:fs,node:path,node:url) -- unambiguous and works in both ESM and CJS. - For huge files or line-by-line work, use streams (
createReadStream) orreadlineinstead of buffering the whole file into memory.
Related: Utility Scripts -- file I/O, paths, globs, and script recipes
7. Parse CLI Arguments
Use node:util's parseArgs -- built-in, typed, no extra dependency.
// greet.ts
import { parseArgs } from "node:util";
const { values } = parseArgs({
options: {
name: { type: "string", short: "n", default: "world" },
shout: { type: "boolean", short: "s", default: false },
},
});
const greeting = `Hello, ${values.name}!`;
console.log(values.shout ? greeting.toUpperCase() : greeting);Run:
npx tsx greet.ts --name Ada --shout
# HELLO, ADA!parseArgsis built into Node 18+ -- no need foryargsorcommanderfor most scripts.short: "n"lets callers pass-n Adainstead of--name Ada.- Returns
values(parsed flags) andpositionals(remaining args) -- both fully typed from the schema. - For larger CLIs with subcommands, help text, and prompts, graduate to
commanderoroclif.
Related: Utility Scripts -- CLI patterns, prompts, exit codes
8. EventEmitter
Pub/sub inside a single process -- the foundation for streams, HTTP servers, and custom events.
// pubsub.ts
import { EventEmitter } from "node:events";
interface Events {
login: [userId: string];
logout: [userId: string];
}
const bus = new EventEmitter<Events>();
bus.on("login", (userId) => {
console.log(`User ${userId} logged in`);
});
bus.emit("login", "u_123");
bus.emit("login", "u_456");emit(name, ...args)calls every listener registered withon(name, handler)in registration order.- Use the typed
EventEmitter<T>generic (Node 22+) to get compile-time checking of event names and payloads. - Always unbind listeners with
off(name, handler)in long-running processes -- forgotten listeners leak memory. onadds a persistent listener;oncefires then removes itself -- pick the right one to avoid duplicate handlers.
Related: EventEmitter Patterns -- typed events, leaks, async iteration
Intermediate Examples
9. Minimal HTTP Server with node:http
Zero dependencies -- serve JSON from the built-in http module.
// server.ts
import { createServer } from "node:http";
const server = createServer((req, res) => {
if (req.url === "/health") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true, uptime: process.uptime() }));
return;
}
res.writeHead(404).end("Not Found");
});
const port = Number(process.env.PORT ?? 3000);
server.listen(port, () => {
console.log(`http://localhost:${port}`);
});createServerreturns anEventEmitter-- you can also listen to"request","connection","close"directly.- Call
res.writeHeadbeforeres.end;res.endcloses the connection. - For anything more than a health endpoint (routing, middleware, validation), reach for Express or Fastify.
- Handle
process.on("SIGTERM", () => server.close(...))so container orchestrators can shut you down gracefully.
Related: Making HTTP Servers -- node:http, Express, and Fastify side by side
10. Express Server with TypeScript
Familiar API, huge ecosystem -- the default choice for most Node.js services.
npm install express
npm install --save-dev @types/express// app.ts
import express, { type Request, type Response } from "express";
const app = express();
app.use(express.json());
interface CreateUserBody {
email: string;
}
app.post("/users", (req: Request<{}, unknown, CreateUserBody>, res: Response) => {
const { email } = req.body;
if (!email?.includes("@")) {
return res.status(400).json({ error: "Invalid email" });
}
res.status(201).json({ id: crypto.randomUUID(), email });
});
app.listen(3000, () => console.log("http://localhost:3000"));express.json()parsesapplication/jsonrequest bodies -- without it,req.bodyisundefined.- Type the
Requestgenerics (Params,ResBody,ReqBody,Query) so handlers are fully typed end-to-end. - Use
zodorvalibotinside the handler to validate bodies instead of trusting the client. - For a faster alternative with built-in schema validation, use Fastify -- same mental model, better throughput.
Related: Making HTTP Servers -- Express vs Fastify vs node:http tradeoffs | ESLint for Node.js Scripts -- recommended rules for server code
11. ESLint Flat Config for TypeScript Scripts
Modern ESLint 9+ uses eslint.config.js (flat config) -- leaner, faster, type-aware.
npm install --save-dev \
eslint typescript-eslint @eslint/js globals// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import globals from "globals";
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: { projectService: true },
globals: { ...globals.node },
},
rules: {
"no-console": ["warn", { allow: ["warn", "error"] }],
},
},
);Run:
npx eslint .
npx eslint . --fix- Flat config is a single file that replaces
.eslintrc.*and.eslintignore-- much less to learn. recommendedTypeCheckedturns on type-aware rules (needsprojectService: true), which catch unsafeanyand promise misuse.globals.nodeteaches ESLint aboutprocess,Buffer,__dirname, etc., so they do not trigger "no-undef".- Pair with a
lintnpm script so CI can fail fast on style and type errors.
Related: ESLint for Node.js Scripts -- rule recommendations, type-aware config, ignore patterns | ESLint Setup (Linting & Formatting) -- broader ESLint reference