npm, Packages & Build Tools
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:
node -v
npm -vA 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:
mkdir marks-app
cd marks-app
npm init -yThe -y flag accepts the default answers. After installing a couple of packages and adding some scripts, a typical file looks like this:
{
"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"
}
}nameandversionidentify the project."private": truestops it from being published to the registry by accident."type": "module"tells Node.js to treat.jsfiles as ES modules, soimportandexportwork (see Modules).scriptsholds named commands for the project.dependenciesanddevDependencieslist the packages, with a version range for each.
Installing packages
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.jsonThe difference between the two lists matters:
dependencies | devDependencies | |
|---|---|---|
| Used | When the app runs in the browser or on the server | Only while you develop, test and build |
| Examples | A date library, a chart library, React | Vite, Vitest, ESLint, Prettier, TypeScript |
| Install flag | none | --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:
import dayjs from "dayjs";
console.log(dayjs("2026-10-30").format("DD MMM YYYY")); // 30 Oct 2026A 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:
| Range | Means | Example |
|---|---|---|
^4.17.21 | Any 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.21 | Any 4.17.x version from 4.17.21 up (same minor) | 4.17.22, but not 4.18.0 |
4.17.21 | Exactly this version | only 4.17.21 |
npm install writes ^ ranges by default. This simplified version of the rules shows which published versions each range accepts:
// 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"));^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.jsonto Git together withpackage.json. - Use
npm installwhile developing; it respects the lockfile and updates it when you add or change packages. - Use
npm cion build servers and for deployments. It deletesnode_modulesand installs exactly what the lockfile says, failing if the lockfile andpackage.jsondisagree. npm outdatedlists packages with newer versions;npm updateupdates within the allowed ranges.npm auditchecks 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.
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>:
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.
npm create vite@latest marks-site -- --template vanilla
cd marks-site
npm install
npm run devThe 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:
npm run build # writes the optimised site to the dist/ folder
npm run preview # serves dist/ locally so you can check the real buildUpload 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:
| Tool | Era | What it did | Today |
|---|---|---|---|
| Bower | About 2012 to 2017 | A separate package manager just for front-end libraries | Retired; use npm |
| Grunt, Gulp | About 2012 to 2018 | Task runners that ran steps such as minifying and copying files | npm scripts and Vite cover most uses |
| Browserify | About 2011 to 2016 | Let browsers use Node-style require() modules | Replaced by ES modules and bundlers |
| Webpack | 2012 onwards | Powerful, highly configurable bundler | Still common in existing projects; new projects usually choose Vite |
| Create React App | 2016 to 2025 | A ready-made React setup built on Webpack | Deprecated 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. Commitpackage.jsonandpackage-lock.jsoninstead. - Putting build tools in
dependencies. Vite, ESLint and test frameworks belong indevDependencies. - 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 withnpxor 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.jsonlists a project's scripts,dependencies(needed at runtime) anddevDependencies(needed while developing).- Versions follow semver,
MAJOR.MINOR.PATCH;^allows new minor and patch versions,~only new patch versions. - Commit
package-lock.jsonand usenpm cifor exact, repeatable installs. npxruns package commands, andnpm 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
{
"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"
}
}# on a laptop
npm install
npm run dev
# on a build server
npm ci
npm test
npm run buildThe 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?
It is only needed while developing and testing, not when the app runs.
2. Which versions does the range ^2.4.1 allow?
The caret allows new minor and patch versions within the same major version.
3. What is the main purpose of package-lock.json?
The lockfile pins exact versions, including the dependencies of your dependencies.
4. What does npm run build usually do in a Vite project?
The build script runs vite build, which bundles and minifies the site into the dist/ folder.
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
