Skip to content

Running JavaScript

The smallest Node.js program is a text file:

js
// hello.js
const name = process.argv[2] ?? "world";
console.log(`Hello, ${name}!`);

Run it from a terminal:

sh
node hello.js Ada
text
Hello, Ada!

That one command already involves several layers. Your shell starts the node executable. Node creates a JavaScript environment, loads hello.js, asks its engine to parse and execute the source, supplies the process and console globals, and forwards the program's output back to the terminal.

Understanding that path makes larger repositories less mysterious. Most project tooling ultimately prepares files and starts a runtime with a particular entry point.

Files and the REPL serve different purposes

Running node without a file opens a read-evaluate-print loop:

text
$ node
> const values = [3, 5, 8]
undefined
> values.map(value => value * 2)
[ 6, 10, 16 ]

The REPL is useful for checking syntax, inspecting an API, or trying a transformation. Its session has special interactive behavior and is not a substitute for a module checked into a repository.

A source file gives code a stable location and module identity. Relative imports are resolved from that location rather than from whichever directory happened to launch an interactive session.

Node can also evaluate a short expression directly:

sh
node --eval 'console.log(6 * 7)'

Use that for disposable checks. Once an experiment needs imports, multiple steps, or explanation, a temporary file is usually clearer than increasingly elaborate shell quoting.

A Node program receives a process environment

Node exposes the current process through process. Command-line arguments appear in process.argv:

js
console.log(process.argv);

For a command such as:

sh
node report.js january.json --format=json

the array starts with the Node executable and the script path. Application arguments begin at index two:

js
const [inputPath, ...options] = process.argv.slice(2);

console.log(inputPath); // "january.json"
console.log(options); // ["--format=json"]

This convention is a host detail, not JavaScript syntax. A browser module has no process.argv because it was not started as a command-line process.

Environment variables are available through process.env:

js
const logLevel = process.env.LOG_LEVEL ?? "info";

Treat them as external strings that may be absent or invalid. They are not a typed configuration system, and secrets should not be printed casually.

Node processes also expose standard input, output, and error streams:

js
process.stdout.write("ordinary output\n");
process.stderr.write("diagnostic output\n");

console.log normally writes to standard output and console.error to standard error. That distinction matters for command-line tools: a user may pipe the successful output into another program while still seeing errors in the terminal.

sh
node report.js data.json > report.txt

A process reports success or failure with an exit status. Assigning process.exitCode lets pending output finish naturally:

js
if (!inputPath) {
  console.error("Usage: node report.js <input-file>");
  process.exitCode = 2;
}

Calling process.exit() terminates immediately and can cut off asynchronous output. Reserve it for cases that truly require immediate termination.

The current directory is not the module directory

Two locations are easy to confuse:

  • The current working directory is where the process was launched.
  • A module's location is where its source file lives.
js
console.log(process.cwd());
console.log(import.meta.url);

If /projects/app contains src/report.js, both of these commands load the same file:

sh
cd /projects/app
node src/report.js
sh
cd /projects
node app/src/report.js

But process.cwd() differs. A user-supplied path is normally interpreted relative to the working directory, because that matches shell conventions. A file shipped beside a module should be located relative to import.meta.url.

Node APIs accept URL objects for many file operations:

js
import { readFile } from "node:fs/promises";

const templateUrl = new URL("./template.txt", import.meta.url);
const template = await readFile(templateUrl, "utf8");

Do not construct module-relative paths by assuming the program is always launched from the repository root.

Modules need an explicit interpretation

Modern JavaScript has two module systems you will encounter in Node repositories:

  • ECMAScript modules use import and export.
  • CommonJS uses require, module.exports, and exports.

This course writes new code as ECMAScript modules. A minimal module pair looks like this:

js
// math.js
export function mean(values) {
  const total = values.reduce((sum, value) => sum + value, 0);
  return total / values.length;
}
js
// index.js
import { mean } from "./math.js";

console.log(mean([3, 4, 8])); // 5

In Node, a nearby package.json can declare that .js files are ECMAScript modules:

json
{
  "type": "module"
}

Explicit alternatives are .mjs for an ECMAScript module and .cjs for CommonJS. A project-wide "type" field is usually less noisy than naming every ordinary module .mjs.

Node's current loader can detect module syntax in some otherwise ambiguous files, but relying on detection makes the project's intent less clear. An explicit "type": "module" also prevents tools and future maintainers from guessing.

Relative ECMAScript imports in Node include the file extension:

js
import { mean } from "./math.js";

Write the specifier that the runtime will load rather than omitting .js because another language's import system performs extension search.

CommonJS remains important because much of the Node ecosystem was built with it:

js
// legacy.cjs
const { readFile } = require("node:fs/promises");

module.exports = {
  loadText: path => readFile(path, "utf8"),
};

You need to recognize that form and understand interoperability errors. You do not need to begin new course programs with it.

package.json describes a package boundary

A JavaScript package is a directory tree described by package.json. For an application, a useful early manifest might be:

json
{
  "name": "expense-report",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "test": "vitest"
  }
}

The fields have distinct jobs:

  • name identifies the package.
  • private prevents accidental publication through npm.
  • type controls how Node interprets .js files in this package.
  • scripts gives repository commands stable names.

Running:

sh
npm run start

asks npm to execute the start script. npm is acting as a script runner here; Node still executes the program.

Package scripts are convenient because they use executables installed locally in node_modules/.bin. Team members do not need compatible global installations of every tool.

Dependencies are recorded, installed, and locked

Suppose a program uses a package named picocolors:

sh
npm install picocolors

npm adds it to dependencies and installs the selected package graph:

json
{
  "dependencies": {
    "picocolors": "^1.1.1"
  }
}

Application code can then import it by package name:

js
import colors from "picocolors";

console.error(colors.red("Invalid input"));

A version range such as ^1.1.1 describes which future versions the manifest permits. package-lock.json records the concrete dependency graph npm selected. Commit both files:

  • package.json expresses the direct intent and allowed ranges.
  • package-lock.json makes installation reproducible and records transitive packages.

Use a development dependency for a tool needed to build, check, or test the repository but not imported by the running production application:

sh
npm install --save-dev vitest

The difference is especially important for publishable packages. For a private application, it still communicates architecture and helps deployment tools choose what to install.

The node_modules directory is generated and normally ignored by Git. The lockfile is not a replacement for installed files; it is the recipe npm uses to reconstruct them.

Runtime, package manager, test runner, and bundler are roles

A traditional Node setup might use:

  • Node.js to execute server or command-line code.
  • npm to install packages and run scripts.
  • Vitest to run tests.
  • TypeScript to check and compile typed source.
  • Vite or another bundler to prepare browser assets.
  • ESLint to analyze source for suspicious patterns.
  • Prettier to format source.

This is not accidental duplication. Each tool answers a different question.

Bun packages several roles into one executable:

sh
bun install
bun run index.js
bun test
bun build ./src/index.tsx

That can be convenient. It can also make it easier to forget which behavior is a runtime feature, which is package resolution, and which is a source transform. Node is used first in this course so those seams remain visible. Later, comparing the same program under Bun will be informative rather than magical.

Browsers run modules through URLs

A browser entry point begins in HTML:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Greeting</title>
  </head>
  <body>
    <script type="module" src="./main.js"></script>
  </body>
</html>
js
// main.js
import { greeting } from "./greeting.js";

document.body.append(greeting("Ada"));

The browser loads main.js as a module URL, follows its imports, and supplies browser APIs. Opening local files directly can encounter origin and module-loading restrictions, so browser development normally uses a small local HTTP server.

Browsers do not natively resolve an npm package name such as "react" using Node's node_modules conventions. Import maps can map bare specifiers, while development tools such as Vite commonly resolve packages and transform source for you. That tooling arrives later; the important point is that module syntax can be shared while resolution belongs to the host or build system.

A modest program structure

A small command-line application does not need an elaborate architecture. It benefits from separating its entry point from reusable behavior:

text
expense-report/
├── package.json
├── package-lock.json
├── src/
│   ├── index.js
│   ├── parse-transactions.js
│   ├── build-report.js
│   └── format-report.js
└── test/
    └── build-report.test.js

src/index.js can know about process.argv, files, and exit codes. build-report.js can accept values and return values without knowing how they entered the process. That separation makes the core easy to test and eventually reusable in a browser or another runtime.

Do not create directories named services, managers, or utils before the code has meaningful concepts to put there. JavaScript modules are inexpensive; add a boundary when it clarifies ownership, isolates an effect, or creates a useful public API.

You now have enough environment context to run the language examples that follow. The next article turns to JavaScript's everyday values and syntax, moving quickly through familiar constructs while spending time on the semantics that differ from their appearance.

Further reference