JavaScript Tooling: npm, Vite and Linting
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
Writing code that works on your machine and shipping code that runs on someone else’s are different skills. Between them sits tooling: how dependencies are installed, how scripts are run, how mistakes are caught, and what actually gets deployed. This chapter sets up the smallest toolchain that covers all four.
Starting point
Node.js 22 LTS ships npm 10. npm create vite@latest scaffolds a project interactively; a manual start is just as good and shows what the generator hides:
mkdir my-app && cd my-app
npm init -y
npm install -D vite
package.json fields worth understanding
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier --write ."
},
"dependencies": {
"date-fns": "^4.1.0"
},
"devDependencies": {
"vite": "^6.0.0"
},
"engines": {
"node": ">=22.12"
}
}
private: trueprevents an accidentalnpm publish.type: modulemakes.jsfiles ESM. Decide per package and stay consistent.dependenciesvsdevDependencies: libraries your code imports at runtime go in the first, tools that only build or check go in the second. A bundled frontend ships only what it imports, but on the server the distinction decides what a production install pulls in.enginesis advisory. Enforce it withengine-strict=truein.npmrcor a version check in CI, otherwise the field is documentation.
Versions and the lockfile
^4.1.0 allows any 4.x; ~4.1.0 allows 4.1.x; 4.1.0 is pinned.
package.jsonrecords ranges;package-lock.jsonrecords the resolved tree with exact versions and integrity hashes.- Commit the lockfile. Without it, two machines resolve different trees and “works on my machine” begins.
npm installmay update the lockfile;npm ciinstalls exactly what is locked and fails ifpackage.jsonand the lockfile disagree. CI and deployments usenpm ci.
npm install # add a dependency locally
npm install -D vite # add a dev dependency
npm ci # clean, reproducible install
npm outdated # what is behind
npm ls react # who pulled this package in
Deduplication matters for more than disk space: two copies of a stateful library mean two states, which shows up as a callback that never fires.
npm scripts
npm run <name> runs a script. Without run, only start, test, stop and restart are shortcuts, so always type run.
{
"scripts": {
"dev": "vite",
"build": "vite build",
"check": "npm run lint && tsc --noEmit"
}
}
node_modules/.binis onPATHinside scripts, soviteworks without a path.npm run build -- --mode stagingpasses everything after--to the script, not to npm.&&stops at the first failure, which is what you want forcheck.;keeps going but is POSIX-only; if you need “run both, report both” on Windows too, write a small Node script.- Avoid
rm -rf,cp,exportand inline env assignment in scripts if the project must build on Windows and macOS.rimrafandcross-envexist for exactly those two cases.
npx runs a package binary without installing it globally:
npx vite --version
npx prettier --check .
Vite: development and build
npm run dev # dev server on 5173
npm run build # bundle into dist/
npm run preview # serve dist/ locally
import { defineConfig } from "vite";
export default defineConfig({
base: "/", // "/blog/" when deployed under a subpath
build: {
outDir: "dist",
sourcemap: true,
target: "es2022",
},
server: {
port: 5173,
proxy: {
"/api": { target: "http://localhost:3000", changeOrigin: true },
},
},
});
- Dev compiles on demand and pre-bundles bare imports, which is why the server starts in milliseconds.
- Build uses Rollup, emits hashed asset names, and is what you deploy —
dist/, not the source tree. baseis the single most common deployment bug: assets requested from/assets/…on a site served from/app/all 404.server.proxyonly exists in dev. Production needs a real reverse proxy with the same routing.
Environment variables come from .env files and import.meta.env:
import.meta.env.MODE; // "development" or "production"
import.meta.env.BASE_URL; // "/" or the base path
import.meta.env.VITE_API; // only VITE_-prefixed variables are exposed
Only VITE_-prefixed variables reach the bundle, and everything in the bundle is public. An API key placed there is a published API key; the browser devtools show it, and so does a view-source. Secret handling belongs on a server you control.
process.env does not exist in the browser. Code that works in Node and references it fails after bundling, and the error points at an unrelated line.
ESLint and Prettier do different jobs
- ESLint finds problems: unused variables, a
useEffectwithout dependencies,==where===was meant, a promise nobody awaits. - Prettier only formats: indentation, quotes, line width. It has no opinion on correctness.
Running both means turning off the formatting rules ESLint still carries, so the two never disagree:
npm install -D eslint @eslint/js prettier eslint-config-prettier
// eslint.config.js — flat config, the default since ESLint 9
import js from "@eslint/js";
import prettier from "eslint-config-prettier";
export default [
js.configs.recommended,
prettier,
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: 2024,
sourceType: "module",
globals: { window: "readonly", document: "readonly" },
},
rules: {
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"no-console": "warn",
eqeqeq: ["error", "smart"],
},
},
{ ignores: ["dist/", "node_modules/"] },
];
Turn on “format on save” and “fix ESLint on save” in the editor, and most of these problems never reach a commit.
Layout, ignore file, output
my-app/
index.html
src/
main.js
styles.css
public/ # copied verbatim
dist/ # build output, never committed
package.json
vite.config.js
node_modules/
dist/
.env
.env.local
After a build, open dist/ and check three things: the entry HTML points at hashed assets, the paths match where the site is actually served from, and no .env content leaked into a bundle. A green build log proves compilation, not correctness.
Dependency discipline
- Ask first whether the package earns its place. Fifteen lines of your own code, tested, often beats a dependency with its own release cadence.
- Check
npm ls <pkg>for duplicates before debugging “my callback never runs”. - Run
npm audit, but read the advisory before running--force: most findings are in a code path you never call, and a forced major upgrade can break more than it fixes. - Read the changelog before a major upgrade, then run your full
checkandbuild. - Prefer packages with a published
provenanceand few transitive dependencies; you are trusting the whole tree.
Common mistakes
1. Deploying with npm install
The usual source of “it worked yesterday”. CI and deployments run npm ci.
2. Committing node_modules
Thousands of files, platform-specific binaries, and still no reproducibility. The lockfile is what makes installs identical.
3. Git-ignoring the lockfile
Every teammate resolves a different tree and the bug cannot be reproduced. Lockfiles belong in version control — for applications always, for libraries too.
4. Scripts that rely on a global tool
command not found on the next machine. Install the tool as a devDependency and call it through npm run.
5. A base that does not match the deployment path
base: "/" with the site at https://example.com/app/ produces 404s for every JS and CSS file. Check the Network panel before touching the code.
6. Secrets in VITE_ variables
They are compiled into the bundle as plain text. Anything a browser can read is public.
7. Running lint but never build
Type and import errors surface during the bundle step. CI needs both commands; a passing linter is not a passing build.
Summary
package.jsondeclares, the lockfile pins,npm cireproduces.- Scripts belong in
npm run, tools indevDependencies; never assume a global install. - Vite compiles on demand in dev and bundles into
dist/for production;basebreaks deployments more than any other option. - ESLint finds problems, Prettier formats,
eslint-config-prettierstops them fighting. - Frontend environment variables are public; secrets stay on the server.
Next: build a real command-line tool with the pieces from this series.