npm, Packages & Build Tools

JavaScript · Lesson 59 of 61 11 min read Updated

Real JavaScript projects are not just a few files you write yourself. They use packages: ready-made code written by others, such as a date library, a chart library or a test framework. Thousands of developers share their packages through npm, and every modern project has a package.json file that lists what it needs.

In this lesson you will create a project, add packages, understand version numbers and lockfiles, run tools with npx and npm scripts, and build a site with Vite, the most popular modern dev server and bundler. You will also learn to recognise older tools such as Bower, Gulp and Webpack.

Node.js and npm

Node.js runs JavaScript outside the browser. Tools such as ESLint, Prettier, Vite and test runners are JavaScript programs that run on Node.js, so front-end developers need it even if their site has no Node.js server.

npm is both the registry at npmjs.com, which hosts millions of packages, and the command-line tool npm that installs them. It comes with Node.js.

Install Node.js from nodejs.org and pick the version marked LTS (long-term support), which is always an even number such as 24 or 26. Then check both tools in a terminal:

Terminal
node -v
npm -v

A version manager such as nvm (or nvm-windows) or fnm lets you switch Node.js versions between projects.

Creating a project: package.json

Every project starts with a package.json file in its main folder. Create one with:

Terminal
mkdir marks-app
cd marks-app
npm init -y

The -y flag accepts the default answers. After installing a couple of packages and adding some scripts, a typical file looks like this:

JSON
{
  "name": "marks-app",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "node --test"
  },
  "dependencies": {
    "dayjs": "^1.11.13"
  },
  "devDependencies": {
    "vite": "^8.3.5"
  }
}
  • name and version identify the project. "private": true stops it from being published to the registry by accident.
  • "type": "module" tells Node.js to treat .js files as ES modules, so import and export work (see Modules).
  • scripts holds named commands for the project.
  • dependencies and devDependencies list the packages, with a version range for each.

Installing packages

Terminal
npm install dayjs                # a dependency: the app needs it to run
npm install --save-dev vitest    # a devDependency: only needed while developing
npm uninstall dayjs              # remove a package
npm install                      # install everything listed in package.json

The difference between the two lists matters:

dependenciesdevDependencies
UsedWhen the app runs in the browser or on the serverOnly while you develop, test and build
ExamplesA date library, a chart library, ReactVite, Vitest, ESLint, Prettier, TypeScript
Install flagnone--save-dev (or -D)

Packages are downloaded into node_modules, which can hold thousands of files because packages depend on other packages. Never edit it or commit it to Git (list it in .gitignore); anyone can recreate it with npm install.

Once installed, you import a package by its name:

JavaScript
import dayjs from "dayjs";

console.log(dayjs("2026-10-30").format("DD MMM YYYY")); // 30 Oct 2026

A name without ./ or / at the start is called a bare import. Node.js looks it up in node_modules, but browsers cannot, which is one job of a build tool like Vite.

Version numbers and semver

npm packages use semantic versioning (semver): MAJOR.MINOR.PATCH, for example 4.17.21.

  • PATCH (4.17.21) goes up for bug fixes.
  • MINOR (4.17.21) goes up for new features that do not break existing code.
  • MAJOR (4.17.21) goes up for breaking changes; your code may need updating.

In package.json, a symbol before the version says which future versions npm may install:

RangeMeansExample
^4.17.21Any 4.x.x version from 4.17.21 up (same major)4.17.22 and 4.18.0, but not 5.0.0
~4.17.21Any 4.17.x version from 4.17.21 up (same minor)4.17.22, but not 4.18.0
4.17.21Exactly this versiononly 4.17.21

npm install writes ^ ranges by default. This simplified version of the rules shows which published versions each range accepts:

JavaScriptRun it yourself
// A simplified version of npm's caret (^) and tilde (~) rules
function parse(version) {
  const [major, minor, patch] = version.split(".").map(Number);
  return { major, minor, patch };
}

function compare(a, b) {
  return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
}

function allowed(range, version) {
  const base = parse(range.slice(1));
  const v = parse(version);
  if (compare(v, base) < 0) return false; // never older than the base
  if (range.startsWith("~")) return v.major === base.major && v.minor === base.minor;
  if (range.startsWith("^")) {
    if (base.major > 0) return v.major === base.major;
    return v.major === 0 && v.minor === base.minor; // 0.x is treated more strictly
  }
  return false;
}

const published = ["4.17.20", "4.17.21", "4.18.0", "5.0.0"];
for (const range of ["^4.17.21", "~4.17.21"]) {
  const ok = published.filter((v) => allowed(range, v));
  console.log(range.padEnd(9), "accepts", ok.join(", "));
}
console.log("^0.3.1 accepts 0.3.9?", allowed("^0.3.1", "0.3.9"));
console.log("^0.3.1 accepts 0.4.0?", allowed("^0.3.1", "0.4.0"));
Output
^4.17.21  accepts 4.17.21, 4.18.0
~4.17.21  accepts 4.17.21
^0.3.1 accepts 0.3.9? true
^0.3.1 accepts 0.4.0? false

Notice the last two lines: for versions starting with 0., the caret is stricter, because before 1.0.0 even a minor change may break things. (Real npm also handles pre-release versions such as 5.0.0-beta.1, which this sketch ignores.)

Lockfiles: the exact versions

Ranges allow different versions on different days, so two laptops could run different code. To prevent this, npm writes package-lock.json, which records the exact version of every installed package, including the packages your packages depend on.

  • Commit package-lock.json to Git together with package.json.
  • Use npm install while developing; it respects the lockfile and updates it when you add or change packages.
  • Use npm ci on build servers and for deployments. It deletes node_modules and installs exactly what the lockfile says, failing if the lockfile and package.json disagree.
  • npm outdated lists packages with newer versions; npm update updates within the allowed ranges.
  • npm audit checks for packages with known security problems.

Other package managers, pnpm, Yarn and Bun, read the same package.json but use their own lockfiles. Use whichever one the project already uses, and never mix them.

npx and npm scripts

Many packages include command-line programs. npx runs them: it uses the copy in your project's node_modules if there is one, and otherwise downloads the package temporarily.

Terminal
npx eslint .                     # run the project's ESLint
npx prettier . --write           # format every file
npm create vite@latest my-site   # run the "create-vite" starter (same as npx create-vite@latest)

npm scripts give short names to the commands a project uses, so nobody has to remember long options. Run them with npm run <name>:

Terminal
npm run dev     # runs "vite"
npm run build   # runs "vite build"
npm test        # "test" and "start" also work without "run"

Inside a script, installed programs are available directly, which is why "dev": "vite" works without npx. Scripts also document how to run the project.

Vite: the modern dev server and bundler

Browsers can load ES modules directly, so why use a build tool at all?

  • Bare imports like import dayjs from "dayjs" must be turned into real file paths.
  • Hundreds of small files load slowly on mobile networks, so production code is bundled into a few files, minified and given hashed file names such as index-B3x9k2.js, which browsers can cache safely.
  • Developers want instant updates while editing, plus TypeScript and JSX support.

Vite (French for "quick", said "veet") does all of this. During development it serves your files almost unchanged as native ES modules and updates the page instantly when you save, often without losing the page's state (this is called hot module replacement, or HMR). For production it bundles everything. Vite 8, released in March 2026, uses Rolldown, a fast bundler written in Rust. Vite is the default build tool behind many frameworks, including Vue, SvelteKit and Astro, and a common choice for React.

Terminal
npm create vite@latest marks-site -- --template vanilla
cd marks-site
npm install
npm run dev

The starter project contains index.html in the main folder (Vite uses it as the entry point), JavaScript and CSS in src/, and static files such as icons in public/. npm run dev starts a local server, by default at http://localhost:5173/. When you are ready to publish:

Terminal
npm run build     # writes the optimised site to the dist/ folder
npm run preview   # serves dist/ locally so you can check the real build

Upload the dist/ folder to any static hosting. Other templates include vanilla-ts, react, vue and svelte.

Older tools you will meet

Front-end tooling has changed many times. You will still meet these in existing projects:

ToolEraWhat it didToday
BowerAbout 2012 to 2017A separate package manager just for front-end librariesRetired; use npm
Grunt, GulpAbout 2012 to 2018Task runners that ran steps such as minifying and copying filesnpm scripts and Vite cover most uses
BrowserifyAbout 2011 to 2016Let browsers use Node-style require() modulesReplaced by ES modules and bundlers
Webpack2012 onwardsPowerful, highly configurable bundlerStill common in existing projects; new projects usually choose Vite
Create React App2016 to 2025A ready-made React setup built on WebpackDeprecated in 2025; use Vite or a framework

Before package managers, libraries such as jQuery were added with <script> tags in exactly the right order. Older Node.js code uses CommonJS modules (const x = require("x") and module.exports = ...) instead of import and export; Node.js supports both, but new code should use ES modules.

Common mistakes

  • Committing node_modules. Commit package.json and package-lock.json instead.
  • Putting build tools in dependencies. Vite, ESLint and test frameworks belong in devDependencies.
  • Deleting the lockfile to "fix" problems. It hides the real cause and changes versions everywhere. Read the error first.
  • Installing tools globally with -g. Each project should list its own tools so everyone uses the same versions; run them with npx or scripts.
  • Installing packages for tiny tasks. Check whether built-in JavaScript can do it first; every dependency is code you must trust and update.

Key points

  • Node.js runs JavaScript tools on your computer; npm installs packages from the npm registry.
  • package.json lists a project's scripts, dependencies (needed at runtime) and devDependencies (needed while developing).
  • Versions follow semver, MAJOR.MINOR.PATCH; ^ allows new minor and patch versions, ~ only new patch versions.
  • Commit package-lock.json and use npm ci for exact, repeatable installs.
  • npx runs package commands, and npm run <script> runs the project's named commands.
  • Vite (version 8 as of October 2026) gives a fast dev server with hot updates and production bundles in dist/; Bower, Grunt, Gulp and Create React App are legacy.

Exercise

Write the package.json for a small project called train-timetable. It should be private, use ES modules, need the package dayjs at runtime (allow new minor versions of 1.11.13), and need vite (8.3.5 or newer within version 8) and vitest (5.0.5 or newer within version 5) only for development. Add scripts named dev, build, preview and test. Then write the terminal commands that a new teammate would run after cloning the project, first on their laptop and then on a build server.

Show solution
JSON
{
  "name": "train-timetable",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "test": "vitest"
  },
  "dependencies": {
    "dayjs": "^1.11.13"
  },
  "devDependencies": {
    "vite": "^8.3.5",
    "vitest": "^5.0.5"
  }
}
Terminal
# on a laptop
npm install
npm run dev

# on a build server
npm ci
npm test
npm run build

The caret ranges allow minor and patch updates but never a new major version. The build server uses npm ci so it installs exactly the versions in package-lock.json.

Quick Quiz

1. Where should a test framework such as Vitest be listed?

2. Which versions does the range ^2.4.1 allow?

3. What is the main purpose of package-lock.json?

4. What does npm run build usually do in a Vite project?

Learn JavaScript with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call