Modern Tooling
What you'll build
By the end of this lesson you will have scaffolded a Vite project, installed dependencies, run a dev server with hot module replacement, and built a production bundle. You will understand what node_modules, package.json, and package-lock.json are and why the bundler generates a dist/ folder instead of serving your source files directly.
Concepts
What Node.js is (and what it is not)
Node.js is a JavaScript runtime that runs outside the browser, on your laptop or on a server. It uses Chrome's V8 engine to execute JavaScript, but it has access to the file system, the network, and operating system APIs that browsers intentionally block for security reasons.
When you install Node, you also get npm (Node Package Manager), which downloads open-source packages from npmjs.com.
# Check your versions
node --version # e.g., v22.1.0
npm --version # e.g., 10.7.0
Node is not "the backend" by itself. It is a runtime. You use it to run build tools (Vite, webpack, TypeScript compiler), CLIs (create-react-app, Prisma), and, when you choose to, servers (Express, Fastify).
package.json and dependencies
package.json is the manifest for your project. It lists the project's name, version, scripts you can run, and dependencies.
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"vite": "^5.2.0"
}
}
dependencies are packages your app needs to run in production. devDependencies are packages only needed during development (bundlers, linters, type checkers). When you deploy to a server, npm install --production skips devDependencies.
The ^ before a version number means "compatible updates are OK", npm can install 18.3.x or 18.4.x but not 19.x. The exact installed versions are locked in package-lock.json, which you should commit to version control so every developer on your team gets identical packages.
npm install lodash # adds to dependencies
npm install vite --save-dev # adds to devDependencies
npm install # installs everything in package.json
Why bundlers exist
In 2010 you could serve JavaScript as individual <script> tags. As projects grew, people split code into dozens of files, imported libraries from CDNs, and quickly hit two problems:
- Dozens of HTTP requests. Each
<script>tag is a separate request. HTTP/1.1 browsers could only handle 6 concurrent requests per domain, so 30 scripts meant several sequential round trips. - No module system. There was no native way to say "this file depends on that file" without managing a manual load order.
Bundlers solve both: they read your source code, follow import statements, pull in everything from node_modules, and output one (or a few) optimised JS files ready for production. They also minify code (remove whitespace, shorten variable names), tree-shake (remove code that is never used), and generate source maps for debugging.
Modern bundlers are also dev servers, they serve your source files with hot module replacement (HMR), which updates the browser instantly when you save a file, without a full reload.
Vite
Vite (pronounced "veet", French for "fast") is the dominant modern bundler for front-end projects. During development it serves your files using native ES modules in the browser, no bundling step at all, so the dev server starts in milliseconds even in a large project. For production builds it uses Rollup under the hood to produce optimised bundles.
# Scaffold a new Vite project
npm create vite@latest my-app -- --template vanilla
cd my-app
npm install
npm run dev
The --template flag sets the starting template. Options include vanilla, vanilla-ts, react, react-ts, vue, svelte, and more.
After npm run dev, you will see:
VITE v5.2.0 ready in 234 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
Open http://localhost:5173. Any change to a .js or .css file in src/ updates the browser instantly.
The project structure
A freshly scaffolded vanilla Vite project looks like this:
my-app/
index.html ← entry point, Vite reads this
src/
main.js ← your JavaScript entry
style.css ← your CSS entry (imported in main.js)
public/ ← static files, copied as-is to dist/
package.json
package-lock.json
node_modules/ ← never commit this
.gitignore ← should include node_modules/ and dist/
node_modules/ can contain tens of thousands of files and hundreds of megabytes. It is always excluded from git because anyone can recreate it with npm install from package.json.
Hands-on
Let's scaffold a project, make a change, and build it.
Step 1: Create the project.
npm create vite@latest github-finder -- --template vanilla
cd github-finder
npm install
Step 2: Start the dev server.
npm run dev
Open http://localhost:5173. You see the default Vite + Vanilla JS starter.
Step 3: Replace src/main.js with your own code.
Open src/main.js and replace everything with:
import "./style.css";
document.querySelector("#app").innerHTML = `
<h1>GitHub Finder</h1>
<div class="search-bar">
<input type="text" id="username" placeholder="GitHub username" value="torvalds">
<button id="search-btn">Search</button>
</div>
<div id="results"></div>
`;
const btn = document.querySelector("#search-btn");
const input = document.querySelector("#username");
const results = document.querySelector("#results");
async function search() {
const user = input.value.trim();
if (!user) return;
results.textContent = "Loading...";
try {
const res = await fetch(`https://api.github.com/users/${user}/repos?sort=stars&per_page=6`);
if (!res.ok) throw new Error(`${res.status}`);
const repos = await res.json();
results.innerHTML = repos.map(r =>
`<p><a href="${r.html_url}" target="_blank">${r.name}</a>, ${r.stargazers_count} stars</p>`
).join("");
} catch (e) {
results.textContent = `Error: ${e.message}`;
}
}
btn.addEventListener("click", search);
input.addEventListener("keydown", e => e.key === "Enter" && search());
search();
Save the file. The browser updates without a reload, that is HMR. The GitHub finder from lesson 5 is now running inside a proper toolchain.
Step 4: Build for production.
npm run build
This creates a dist/ folder. Look inside it:
dist/
index.html
assets/
index-BxYd3j4k.js ← your code + dependencies, minified, with a hash in the name
index-CkLp8mNq.css ← your CSS, minified
The hash in the filename changes whenever the file content changes, which forces browsers to download the new version instead of using a stale cached copy.
Step 5: Preview the production build locally.
npm run preview
This serves dist/ on http://localhost:4173. This is what the real server will serve after you deploy.
Common pitfalls
- Committing
node_modules/. It is huge, OS-specific, and fully regeneratable. Always have it in.gitignore. Vite's scaffolding adds it by default; check before you first push. - Confusing
npm run devandnpm run build.devgives you a dev server with HMR, not what you deploy.buildcreates the production bundle indist/. Always deploy thebuildoutput. - Not reading error messages. npm errors are verbose but accurate. When
npm installfails, read the last ten lines, it almost always tells you exactly what went wrong (wrong Node version, missing native dependency, etc.). - Mixing
--saveand--save-devcarelessly. Putting Vite or TypeScript independenciesinstead ofdevDependenciesbloats your production bundle if anything bundles it transitively. Use--save-devfor tools that do not run in the browser. - Using
npm civsnpm installon servers. On CI/CD pipelines and servers, prefernpm ci, it installs exactly what is inpackage-lock.jsonwithout modifying it, which is faster and more reproducible.
What to try next
- Scaffold a new project with
--template reactand explore how Vite handles JSX. Look at the difference inmain.jsxcompared tomain.js. - Add a linter. Run
npm install eslint --save-devand thennpx eslint --init. Enable the "recommended" rules and runnpx eslint src/. Fix any warnings it finds. - Change the port the dev server runs on. In
vite.config.js(create it if it does not exist), addexport default { server: { port: 3000 } }. Stop and restart the dev server and notice it now runs on port 3000.
Prefer watching over reading?
Subscribe for free.