Architect Your Workbench: Node.js, npm, and Dual-Project Scaffolding
Learning Objectives
- ✓Install and verify Node.js and npm on your local machine
- ✓Explain the role of package.json, package-lock.json, and node_modules in a JavaScript project
- ✓Scaffold a React frontend project using Vite with the React template
- ✓Initialize an Express backend project from scratch using npm init and install dependencies
- ✓Configure VS Code with essential extensions for full-stack JavaScript development
- ✓Run both frontend and backend development servers simultaneously and verify their output
I'll read through the lesson content carefully to identify the 3 code blocks missing language tags and find the best place to insert the WRONG WAY → BETTER → BEST pattern.
Analysis:
-
Code blocks missing language tags — I found 3:
- The "Start the backend" terminal output block
- The "Start the frontend" terminal output block
- The project directory tree structure block
-
WRONG → BETTER → BEST pattern — Best placement is in the "Initializing the Express Backend" section, right after the nodemon/scripts discussion, covering how developers run their server during development.
Here is the complete lesson with additions integrated:
Architect Your Workbench: Node.js, npm, and Dual-Project Scaffolding
I once watched a junior developer spend an entire Friday trying to debug a "broken" React app. The error messages were cryptic, components refused to render, and nothing made sense. After two hours of pair debugging, we discovered the problem: they had Node.js version 10 installed — a version so old it didn't support the JavaScript syntax their project required. The fix took ninety seconds. The lesson took all day. Your development environment is the foundation beneath everything you build, and a cracked foundation will haunt every floor you add on top of it.
That is why we start here, before a single line of application code. By the end of this lesson, you will have two fully initialized project folders on your machine — taskpilot-client for your React frontend and taskpilot-server for your Express backend — each with a proper package.json, installed dependencies, and a working hello-world script you can run from your terminal. More importantly, you will understand why each piece exists and what it does for you behind the scenes.
The Problem: "It Works on My Machine"
The most expensive bugs in software are environment bugs, because they masquerade as code bugs. You write perfectly valid JavaScript. You follow a tutorial step by step. But nothing works. The error says SyntaxError: Unexpected token or MODULE_NOT_FOUND, and you start questioning your sanity. Nine times out of ten, the issue is not your code — it is your tooling. Wrong Node version. Missing dependency. Corrupted node_modules folder. A project initialized without the right configuration.
At my first startup, we lost two full days of sprint velocity to an environment mismatch. Three developers had Node 14, one had Node 16, and our CI server was running Node 18. A feature worked locally for most of the team, failed for one person, and then exploded in the deployment pipeline. The root cause was a single JavaScript method — Array.prototype.at() — that only exists in Node 16.6 and later. The developer on Node 14 never had access to it. The fix was trivial. The cost of finding it was enormous.
This lesson exists so that never happens to you. We are going to install the right version of Node.js, understand exactly what npm does and why package.json matters, scaffold both halves of our full-stack application, and confirm everything runs. Think of this as building your workbench before you start woodworking. You would never try to build a table on an uneven floor with dull tools — and you should never try to build a web application on a misconfigured machine.
⚠️ Common Pitfall: Skipping environment setup to "get to the real coding faster" is the number one reason beginners abandon projects. The thirty minutes you invest now will save you dozens of hours of mysterious bugs later.
What Is Node.js and Why Does It Matter?
Node.js is a runtime that lets you execute JavaScript outside of a web browser. That single sentence reshapes how modern web development works. For the first fifteen years of JavaScript's existence — from its creation by Brendan Eich at Netscape in 1995 until around 2009 — JavaScript could only run inside a browser. If you wanted to build a server, you used PHP, Ruby, Python, or Java. Your frontend team wrote JavaScript; your backend team wrote something else entirely. Two languages, two ecosystems, two mental models.
In 2009, Ryan Dahl created Node.js and broke that wall down. He took V8, the lightning-fast JavaScript engine that Google built for Chrome, and wrapped it in a standalone program that could run on any computer. Suddenly, JavaScript could read files, listen on network ports, connect to databases, and do everything a "real" server language could do. The practical impact was enormous: a single developer — or a single team — could use one language for the entire application stack. Netflix, LinkedIn, PayPal, Uber, and Walmart all adopted Node.js for their backend services. PayPal reported that rewriting their Java backend in Node.js resulted in 33% fewer lines of code and a 35% decrease in average response time.
JavaScript in the browser is a hand saw — perfectly good for the job it was designed for, but limited in scope. Node.js is the table saw. Same blade material (the V8 engine), but mounted in a machine that gives it far more power, speed, and versatility. When you install Node.js on your computer, you are installing that table saw. You get the node command, which can execute any JavaScript file, and you get npm (Node Package Manager), which is your hardware store — an enormous catalog of pre-built parts that other developers have shared.
You do not need to understand the internals of V8 or the event loop right now. What you need to know is this: Node.js lets you run JavaScript on your computer and on servers. That capability is what makes full-stack JavaScript development possible. Our React frontend will be built using tools that run on Node.js. Our Express backend will be a Node.js application. Node.js is the common ground beneath both halves of our project.
💡 Key Insight: Node.js is not a framework or a library. It is a runtime environment — the engine that executes your JavaScript code. Express, React, and every npm package all run on top of Node.js.
| Term | What It Is | Analogy |
|---|---|---|
| Node.js | JavaScript runtime built on Chrome's V8 engine | The engine in your car |
| npm | Package manager bundled with Node.js | An app store for code libraries |
node command | CLI tool to execute JavaScript files | Double-clicking a .exe file |
npm command | CLI tool to install and manage packages | apt-get or brew for JavaScript |
The two tools you will use most frequently are the node command (to run scripts) and the npm command (to install packages and run project scripts). Let's install them now.
🤔 Think about it: Why would companies like Netflix choose Node.js over established backend languages like Java?
View Answer
Several reasons converge. First, team efficiency: frontend and backend developers share one language, reducing context-switching and making it easier to move engineers between teams. Second, npm's massive ecosystem means you rarely build from scratch. Third, Node.js handles I/O-heavy workloads (like API servers that mostly wait for database responses) extremely well due to its non-blocking, event-driven architecture. Netflix specifically valued the reduced startup time and the ability to share validation code between browser and server.
Installing Node.js and Verifying Your Setup
With a clear picture of what Node.js gives you, the next step is getting it onto your machine.
Always install Node.js from the official source, and always choose the LTS (Long Term Support) version. LTS means the Node.js team will maintain, patch, and support that version for years. The "Current" version has the newest features but may contain breaking changes. For production work and learning, LTS is the right choice. As of this writing, the LTS version is in the 20.x line. Visit nodejs.org, download the LTS installer for your operating system, and run it. The installer will place both node and npm on your system PATH, meaning you can use them from any terminal window.
After installation, open your terminal and verify both tools are available. On macOS, open Terminal or iTerm2. On Windows, open PowerShell or Windows Terminal (not the old cmd.exe — do yourself a favor and upgrade). On Linux, open your preferred terminal emulator. Then run these two commands:
node -v
You should see output like v20.11.0 (your exact version number will vary). Then:
npm -v
You should see something like 10.2.4. If either command returns "command not found" or an error, the installation did not complete correctly — revisit the installer and make sure you allowed it to modify your system PATH.
The version numbers matter more than most beginners realize. If you are following along six months from now and Node 22 is the current LTS, that is perfectly fine. What matters is that you are on a supported LTS release. You can always check which versions are currently under LTS at the Node.js releases page. I personally recommend using a version manager like nvm (Node Version Manager) once you start working on multiple projects, because different projects may require different Node versions. But for this course, a single global installation is all you need.
⚠️ Common Pitfall: On macOS, do not install Node.js using
sudo apt-getor Homebrew's default formula without understanding what you are doing. The official installer fromnodejs.orgis the most reliable path for beginners. Homebrew can work well, but misconfigured Homebrew installations cause more student headaches than any other setup issue I have seen.
Deep Dive: What is nvm and should I use it?
nvm (Node Version Manager) is a shell tool that lets you install and switch between multiple Node.js versions on the same machine. Imagine you are maintaining an older project that requires Node 16 and simultaneously building a new project on Node 20. Without nvm, you would need to uninstall and reinstall Node every time you switched projects. With nvm, you type nvm use 16 or nvm use 20 and the switch happens instantly.
For this course, a single LTS installation is sufficient. But if you plan to work professionally with Node.js, installing nvm early is a smart investment. On macOS and Linux, install it from the official repository at github.com/nvm-sh/nvm. On Windows, use nvm-windows from github.com/coreybutler/nvm-windows. Once installed, you can run nvm install --lts to grab the latest LTS version and nvm alias default lts/* to set it as your default.
💡 Key Takeaway: Install Node.js LTS from
nodejs.org, then verify withnode -vandnpm -v. Both commands must return version numbers before you proceed.
Understanding npm and package.json
Node.js gives you the engine. npm gives you the parts catalog.
npm is the world's largest software registry, and package.json is your project's manifest file — it declares what your project is and what it needs to run. When you install Node.js, npm comes bundled with it for free. npm serves two distinct roles that beginners often conflate: it is both a registry (a massive online database of open-source JavaScript packages, with over 2 million packages as of 2024) and a command-line tool (the npm command you use to install, update, and manage those packages locally).
Think of package.json as a recipe card for your project. A recipe lists the ingredients you need and in what quantities. package.json lists the packages your project depends on and which versions are compatible. Hand someone your recipe card, and they can go to the store, buy the ingredients, and cook the same dish. Hand someone your package.json, and they can run npm install and reconstruct your entire dependency tree. This is why package.json goes into version control (Git) but the node_modules folder does not — node_modules is the pantry full of actual ingredients, which can be rebuilt from the recipe at any time.
Every package.json file has a few critical fields you should understand from day one. The name field is your project's identity — it must be lowercase with no spaces. The version field tracks your project's version using semantic versioning (major.minor.patch). The scripts field is where you define shortcut commands like "start": "node index.js" so you can type npm start instead of the full command. The dependencies field lists packages your application needs to run in production, like Express. The devDependencies field lists packages only needed during development, like testing tools or linters.
| Field | Purpose | Example Value |
|---|---|---|
name | Project identifier (lowercase, no spaces) | "taskpilot-server" |
version | Semantic version of your project | "1.0.0" |
scripts | Named command shortcuts | {"start": "node index.js"} |
dependencies | Packages needed at runtime | {"express": "^4.18.2"} |
devDependencies | Packages needed only in development | {"nodemon": "^3.0.0"} |
The caret (^) symbol in version strings like "^4.18.2" is npm's way of saying "compatible with." It means npm can install version 4.18.2 or any newer version that does not change the major version number (so 4.18.3, 4.19.0, or 4.99.9 are all acceptable, but 5.0.0 is not). This convention follows semantic versioning: if the major number changes, the package may have breaking changes. The caret keeps you on the latest bug fixes and minor features without risking compatibility.
When you run npm install express, three things happen simultaneously. First, npm contacts the registry, resolves the latest compatible version of Express and all of its dependencies (Express depends on about 30 other packages). Second, npm downloads everything into a folder called node_modules in your project directory. Third, npm records what it installed in package.json under dependencies and creates (or updates) a package-lock.json file that pins the exact versions of every package in the tree. The lock file ensures that if your teammate runs npm install tomorrow, they get the exact same versions you have — not whatever happens to be "latest" at that point.
💡 Key Insight: Never commit
node_modulesto Git. It can contain tens of thousands of files and hundreds of megabytes. Commitpackage.jsonandpackage-lock.jsoninstead. Anyone can recreatenode_modulesby runningnpm install.
🤔 Think about it: What would happen if there were no package-lock.json and two developers ran npm install a week apart?
View Answer
They could end up with different versions of sub-dependencies. Imagine your project depends on Express ^4.18.2. When Developer A installs, Express 4.18.2 is the latest, so that is what they get. A week later, Express 4.18.3 is released. Developer B runs npm install and gets 4.18.3. If a bug was introduced in 4.18.3, Developer B's code breaks while Developer A's works fine — the dreaded "works on my machine" problem. package-lock.json prevents this by recording the exact resolved versions so both developers get identical installs.
Setting Up VS Code for Full-Stack Development
Your runtime is installed and you understand how packages work. Now you need a place to write the code itself.
Visual Studio Code is the dominant editor for JavaScript development, and for good reason — its extension ecosystem and integrated terminal make it a full development environment, not just a text editor. According to the Stack Overflow Developer Survey, VS Code has been the most popular development environment for years running, used by over 73% of professional developers. You can download it from code.visualstudio.com. If you prefer another editor like WebStorm or Vim, everything in this course works from any editor and terminal. But my instructions will reference VS Code features, so following along will be easiest if you use it.
After installing VS Code, add these four extensions that will pay for themselves within your first hour of coding. Open VS Code, click the Extensions icon in the left sidebar (or press Ctrl+Shift+X on Windows/Linux, Cmd+Shift+X on macOS), and search for each one:
| Extension | Publisher | Why You Need It |
|---|---|---|
| ESLint | Microsoft | Catches JavaScript errors and style issues as you type |
| Prettier | Prettier | Auto-formats your code on save so you never argue about tabs vs spaces |
| ES7+ React/Redux/React-Native Snippets | dsznajder | Shortcuts for React boilerplate (rafce generates a full component) |
| Thunder Client | Ranga Vadhineni | Test your API endpoints without leaving VS Code (like a lightweight Postman) |
The single most impactful setting change you can make is enabling format-on-save with Prettier. Open VS Code settings (Ctrl+, or Cmd+,), search for "format on save," and check the box. Then search for "default formatter" and select Prettier. From this moment on, every time you save a file, Prettier will automatically fix your indentation, add missing semicolons (or remove them, depending on your config), and standardize your quote style. This removes an entire category of mental overhead. I have Prettier configured on every project I touch, and I have strong opinions about semicolons (use them — implicit insertion rules are a footgun), but we will let Prettier handle that debate for us.
The integrated terminal in VS Code is where you will spend half your time. Open it with Ctrl+` (backtick) or from the menu under Terminal > New Terminal. This terminal runs inside your editor, so you can see your code and your command output simultaneously. You can split it into multiple panes, and it automatically opens in your project's root directory. Get comfortable with this terminal — you will use it to run npm commands, start your development servers, and check Git status throughout this course.
💡 Key Takeaway: VS Code with ESLint, Prettier, React snippets, and Thunder Client gives you a complete full-stack development environment. Enable format-on-save immediately — your future self will thank you.
Deep Dive: Multi-root workspaces in VS Code
Since we are building two separate projects (taskpilot-client and taskpilot-server), you have two options for organizing them in VS Code. Option one: open each project in its own VS Code window. Option two: create a multi-root workspace that includes both folders. To do this, open VS Code, go to File > Add Folder to Workspace, and add both project folders. Then save the workspace as taskpilot.code-workspace. The workspace file remembers which folders are included, so you can reopen everything with one click. I personally prefer the multi-root workspace approach because it lets me search across both projects simultaneously and keeps my taskbar clean. We will use a parent taskpilot/ folder to hold both projects, and you can open that parent folder directly in VS Code for the same effect.
Scaffolding the React Frontend with Vite
Your editor is ready. Now it is time to create something worth editing.
Vite (pronounced "veet," the French word for "fast") is a build tool created by Evan You — the same person who created Vue.js — and it has become the standard way to start new React projects. If you have seen older tutorials that use Create React App (CRA), know that CRA is officially deprecated as of early 2023. The React documentation itself now recommends Vite, Next.js, or Remix for new projects. Vite starts a development server in under 300 milliseconds regardless of project size, compared to CRA's 10–30 second startup. It achieves this speed by using native ES modules in the browser during development, only bundling your code when you build for production.
Where CRA was a moving truck — heavy, slow to start, carrying everything whether you need it or not — Vite is a sports car. It hands you a clean project structure, installs React and ReactDOM, configures a development server with instant hot-reload, and gives you a production build command. All in about ten seconds. The process that used to take an experienced developer an hour and a beginner an entire weekend now takes a single command.
Let us create our parent project folder and scaffold the frontend. Open your terminal (either VS Code's integrated terminal or a standalone terminal) and navigate to where you want your project to live. I typically put projects in a ~/projects directory, but use whatever makes sense for your system:
mkdir taskpilot
cd taskpilot
npm create vite@latest taskpilot-client -- --template react
That third command is doing a lot of work behind the scenes. npm create is a shorthand for npm init that runs a package's initializer script. vite@latest tells npm to use the latest version of the create-vite package. taskpilot-client is the folder name for your new project. The -- separator tells npm that everything after it should be passed to the create-vite tool, and --template react tells Vite to use the React template (as opposed to Vue, Svelte, or vanilla JavaScript). After this command finishes, you will have a taskpilot-client folder with a complete React project inside it.
Next, move into the project directory and install its dependencies:
cd taskpilot-client
npm install
The npm install command reads package.json, downloads every listed dependency, and populates the node_modules folder. For a fresh Vite React project, this installs React, ReactDOM, Vite itself, and the React Vite plugin. It takes anywhere from 5 to 30 seconds depending on your internet connection. Once it finishes, you can start the development server:
npm run dev
Your terminal will display a local URL, typically http://localhost:5173. Open that URL in your browser and you will see the Vite + React starter page with a spinning logo and a counter button. Click the button — the counter increments. This is your React app running with hot module replacement. Edit src/App.jsx and save the file; your browser updates instantly without a full page reload. This tight feedback loop is one of Vite's killer features and it will make the development process feel effortless.
Press Ctrl+C in your terminal to stop the development server. Navigate back up to the parent folder:
cd ..
⚠️ Common Pitfall: If
npm create vite@latestfails with a permission error on macOS or Linux, do NOT usesudo. Permission errors with npm usually mean your global npm directory has wrong ownership. Fix it by runningnpm config set prefix ~/.npm-globaland adding~/.npm-global/binto your PATH, or usenvmwhich avoids global permission issues entirely.
🤔 Think about it: Why does the Vite command use --template react instead of just detecting what framework you want automatically?
View Answer
Vite is framework-agnostic by design. It supports React, Vue, Svelte, Preact, Lit, and vanilla JavaScript/TypeScript. Each template sets up different dependencies, configuration, and file structures. By making the template explicit, Vite avoids assumptions and keeps itself modular. This is also why Vite has gained adoption across multiple framework communities — it is not married to React the way Create React App was.
Initializing the Express Backend from Scratch
The frontend is scaffolded and ready. Now for the other half — and this time, we are building by hand.
This is intentional. I want you to understand every file and every configuration choice in your server project, because when something goes wrong at 2 AM in production, you need to know where to look. Scaffolding tools are wonderful for the frontend, where the build tooling is complex and opinionated. But an Express server is simple enough that building it yourself is faster than learning someone else's template.
Create the backend project folder and initialize it with npm:
mkdir taskpilot-server
cd taskpilot-server
npm init -y
The npm init -y command creates a package.json file with all default values. The -y flag means "yes to everything" — it accepts all the defaults instead of asking you questions about project name, version, description, and so on. You will get a package.json that looks roughly like this, with the name automatically derived from the folder name.
Now install Express, the backend framework we will use throughout this course:
npm install express
Express is the most widely used Node.js web framework, with over 30 million weekly downloads on npm. Created by TJ Holowaychuk in 2010, it has been the backbone of countless production applications. Express provides a thin layer on top of Node.js's built-in HTTP server, giving you routing (mapping URLs to handler functions), middleware (pluggable functions that process requests), and a clean API for sending responses. It deliberately avoids being an "everything framework" like Ruby on Rails or Django — it gives you just enough structure to be productive while letting you choose your own database, templating engine, and authentication strategy.
Let us also install nodemon as a development dependency:
npm install --save-dev nodemon
Nodemon watches your files and automatically restarts your server whenever you save a change. Without it, every time you modify your server code, you would need to manually stop the server (Ctrl+C) and restart it (node index.js). With nodemon, the restart happens automatically in milliseconds. The --save-dev flag tells npm to record nodemon under devDependencies instead of dependencies, because nodemon is a development convenience — your production server does not need it.
Now open package.json in your editor and update the scripts section:
Replace the existing "test" script line with:
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
These two scripts define your production and development workflows. npm start runs your server with plain node, as you would in production. npm run dev runs it with nodemon, giving you automatic restarts during development. Notice that start is a special npm script name — you can run it as just npm start without the run keyword. All other script names require npm run <name>.
❌ WRONG WAY → 🤔 BETTER → ✅ BEST: Running Your Server During Development
This is a mistake I see constantly with beginners. Let me show you the progression from painful to professional when it comes to running your Express server while actively developing.
❌ WRONG WAY: Manually restarting the server every time
node index.js
# Make a code change... nothing happens
# Ctrl+C to kill the server
node index.js
# Make another change... nothing happens again
# Ctrl+C
node index.js
# Repeat 200 times per day and lose your mind
// You also hardcode the port with no environment override
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Hello' });
});
app.listen(3001, () => {
console.log('Server running');
});
This is the most common approach I see from developers following outdated tutorials. You change a line of code, Alt-Tab to the terminal, press Ctrl+C, press the up arrow, press Enter, and Alt-Tab back to your editor. You do this hundreds of times per day. It is slow, it breaks your flow, and you will inevitably forget to restart and spend ten minutes debugging code that is "not working" — when in reality the server is still running the old version.
🤔 BETTER: Using Node's built-in --watch flag
node --watch index.js
# Server restarts automatically on file changes!
// Port is configurable, but the script is not in package.json
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3001;
app.get('/', (req, res) => {
res.json({ message: 'Hello' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
This is a real improvement — Node 18.11+ has a built-in --watch flag that auto-restarts on file changes. No extra dependency needed. But this approach has two drawbacks: the flag is still marked as experimental in many Node versions, meaning its behavior could change, and you are typing the full command every time instead of using a named script. Your teammate has to know the exact invocation to run the project.
✅ BEST: nodemon with named npm scripts and environment-aware port
npm run dev
# That's it. One command. Everyone on the team runs the same thing.
// Proper setup: environment-aware, documented, production-ready
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3001;
app.get('/', (req, res) => {
res.json({ message: 'Hello' });
});
app.listen(PORT, () => {
console.log(`Server listening on http://localhost:${PORT}`);
});
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
}
This is the professional setup. npm run dev is a single, memorable command that any developer can run without knowing the underlying tool. nodemon is battle-tested and stable across all Node versions. The start script provides a clean production entry point. The port reads from the environment so deployment platforms can override it. Your development workflow and your production workflow are both defined in one place — package.json — and anyone who clones the repo can run the project without reading a single line of source code.
Now navigate back to the parent folder:
cd ..
💡 Key Insight: Frontend tooling (Vite, Webpack, Parcel) is complex because browsers have strict requirements about how code is bundled and served. Backend tooling is simpler because Node.js runs your files directly. This is why we scaffolded the frontend but hand-built the backend.
| Aspect | taskpilot-client (Frontend) | taskpilot-server (Backend) |
|---|---|---|
| Created with | npm create vite@latest | npm init -y |
| Framework | React 18+ | Express 4 |
| Dev command | npm run dev (Vite server) | npm run dev (nodemon) |
| Dev server port | 5173 (default) | 3001 (we will configure) |
| Build step needed | Yes (Vite bundles for production) | No (Node runs source directly) |
This table highlights a fundamental difference between frontend and backend JavaScript. The frontend must be compiled, bundled, and optimized before browsers can run it. The backend runs directly on Node.js with no build step required. Keep this distinction in mind — it will explain many of the tooling differences you encounter throughout this course.
🤔 Think about it: Why do we separate the frontend and backend into two different project folders instead of one monolithic project?
View Answer
Separation gives you independent deployment, independent scaling, and cleaner mental models. In production, your React frontend might be served from a CDN like Cloudflare or Vercel, while your Express backend runs on a different server entirely. They communicate over HTTP, not file imports. Keeping them separate from day one mirrors this production reality. It also means different team members can work on frontend and backend simultaneously without merge conflicts in shared configuration files. Companies like Airbnb and Shopify maintain separate repositories (or at minimum separate folders in a monorepo) for their frontend and backend code.
Variations and Trade-offs: Choosing Your Tooling
Every tool choice in this lesson has alternatives, and understanding the trade-offs will make you a better engineer. Here are the decisions I made and why — with full transparency about where reasonable people disagree.
For the frontend scaffolding tool, Vite is my clear winner, but Next.js is the main competitor. Next.js is a full React framework that includes server-side rendering, file-based routing, API routes, and much more. It is extremely powerful and is the right choice for production applications that need SEO, server rendering, or a backend-in-one approach. But for learning React fundamentals, Next.js adds too much abstraction too early. You would be learning Next.js concepts before understanding the React concepts they are built on. Vite gives you a clean React setup with zero magic — you see the React code and nothing else.
For the backend framework, Express wins on ecosystem maturity, but Fastify is worth knowing about. Fastify is a newer Node.js framework that is significantly faster than Express in raw benchmarks — roughly 2–3x more requests per second in synthetic tests. It also has built-in schema validation using JSON Schema and a powerful plugin system. However, Express has a larger ecosystem of middleware, more Stack Overflow answers, more tutorials, and more production battle-testing. When you are learning, community support matters more than raw throughput. We are building a task manager, not a system handling 100,000 requests per second.
| Decision | Our Choice | Alternative | Why We Chose This |
|---|---|---|---|
| Frontend scaffolding | Vite | Next.js, Create React App | Minimal abstraction, fast dev server, React-focused |
| Backend framework | Express | Fastify, Koa, Hono | Largest ecosystem, most learning resources, battle-tested |
| Package manager | npm | Yarn, pnpm | Ships with Node.js, zero extra installation, good enough |
| Dev auto-restart | nodemon | tsx --watch, node --watch | Most widely documented, reliable across Node versions |
A note on npm versus Yarn and pnpm. Yarn (created by Facebook in 2016) and pnpm (which uses a content-addressable store to save disk space) are both excellent package managers with genuine technical advantages over npm. Yarn introduced lock files before npm did. pnpm installs packages faster and uses less disk space. But npm has closed the gap significantly, and it comes pre-installed with Node.js. For this course, using npm means one fewer tool to install and one fewer potential source of confusion. If you later work at a company that uses Yarn or pnpm, the concepts transfer directly — only the command names change slightly.
💡 Key Takeaway: Vite + Express + npm is the pragmatic choice for learning full-stack development. Each tool has competitors with specific advantages, but this stack maximizes community support and minimizes accidental complexity.
Deep Dive: What about Node.js's built-in --watch flag?
Starting with Node.js 18.11, you can run node --watch index.js and get automatic restarts without installing nodemon at all. As of Node 20, this feature is still marked as experimental, but it works well for simple cases. I chose nodemon for this course because it has more configuration options (like ignoring specific files or watching additional file extensions), it is stable and battle-tested, and it appears in the vast majority of existing tutorials and documentation you will encounter. Once --watch graduates from experimental status, it will likely replace nodemon for most use cases. Feel free to try it — just replace "dev": "nodemon index.js" with "dev": "node --watch index.js" in your package.json.
Deep Dive: Code Walkthrough — Your First Full-Stack Hello World
With both projects initialized, let us write actual code and see it run. This walkthrough creates a working Express server and modifies the Vite React app so both halves of your project produce verifiable output.
// ============================================================
// FILE: taskpilot-server/index.js
// PURPOSE: Minimal Express server — your backend starting point
// RUN: cd taskpilot-server && npm run dev
// ============================================================
// 1. Import Express using require() — Node.js's module system
// We'll switch to ES modules (import/export) later in the course
const express = require('express');
// 2. Create an Express application instance
// This 'app' object is where you define routes and middleware
const app = express();
// 3. Define the port your server will listen on
// process.env.PORT lets deployment platforms (Heroku, Railway)
// override this value; 3001 is our local development default
const PORT = process.env.PORT || 3001;
// 4. Define a route: when someone visits the root URL ('/'),
// respond with a JSON object. JSON is the universal language
// of web APIs — both our React frontend and any mobile app
// can parse it.
app.get('/', (req, res) => {
res.json({
message: 'TaskPilot API is running',
status: 'healthy',
timestamp: new Date().toISOString()
});
});
// 5. Define a second route for a future health-check endpoint
// Load balancers and monitoring tools hit endpoints like this
// to verify your server is alive
app.get('/health', (req, res) => {
res.json({
uptime: process.uptime(),
status: 'ok'
});
});
// 6. Start the server — this is what actually opens the port
// and begins accepting HTTP connections
app.listen(PORT, () => {
console.log(`TaskPilot server listening on http://localhost:${PORT}`);
console.log(`Health check: http://localhost:${PORT}/health`);
});
// ============================================================
// EXPECTED OUTPUT when you run `npm run dev`:
// ============================================================
// TaskPilot server listening on http://localhost:3001
// Health check: http://localhost:3001/health
//
// Then visit http://localhost:3001 in your browser. You'll see:
// {"message":"TaskPilot API is running","status":"healthy","timestamp":"<current ISO timestamp>"}
//
// Visit http://localhost:3001/health and you'll see:
// {"uptime":<seconds since server started>,"status":"ok"}
// ============================================================
Let me walk through every significant decision in this file. Line by line, there are no mysteries here — and that is the goal.
The require('express') call on line 8 uses Node.js's CommonJS module system. You might have seen import express from 'express' in other tutorials — that is ES module syntax, the modern standard. Both work in Node.js, but CommonJS (require) works out of the box without any configuration. We will migrate to ES modules in a later lesson when we set up our project more formally. For now, require gets us running with zero friction.
The app.get('/', callback) pattern on line 19 is the core of Express routing. The first argument is the URL path. The second is a callback function that receives two objects: req (the incoming request, containing headers, query parameters, body, etc.) and res (the response object, which you use to send data back to the client). res.json() automatically sets the Content-Type header to application/json and serializes your JavaScript object into a JSON string. This is Express handling the tedious plumbing so you do not have to.
The PORT variable on line 14 uses a pattern you will see in every Node.js project. process.env.PORT reads an environment variable called PORT. When you deploy to platforms like Railway, Render, or Heroku, they inject this variable to tell your app which port to use. The || 3001 part provides a fallback for local development. I chose 3001 instead of the more common 3000 because many React tutorials use 3000 for the backend — using 3001 avoids port conflicts if you have another project running.
To run this, make sure you are in the taskpilot-server directory and run npm run dev. You should see the two console.log messages in your terminal. Open http://localhost:3001 in your browser — you will see a JSON response. Open http://localhost:3001/health to see the health check endpoint. Every time you refresh /health, the uptime value will be larger, confirming your server is alive and counting.
📌 Remember: The Express server does NOT auto-refresh your browser. Nodemon restarts the server process when files change. To see updated responses, you refresh your browser manually (or use Thunder Client in VS Code).
🔨 Project Update
This is lesson 1, so everything below is new. After completing this lesson, your project directory structure should look like this:
taskpilot/
├── taskpilot-client/ # React frontend (scaffolded by Vite)
│ ├── node_modules/ # Installed dependencies (do NOT commit)
│ ├── public/ # Static assets
│ ├── src/ # Your React source code
│ │ ├── App.jsx # Main application component
│ │ ├── App.css # Component styles
│ │ ├── main.jsx # Entry point (renders App into the DOM)
│ │ └── index.css # Global styles
│ ├── index.html # The single HTML page React mounts into
│ ├── package.json # Frontend dependencies and scripts
│ ├── package-lock.json # Pinned dependency versions
│ └── vite.config.js # Vite configuration
│
├── taskpilot-server/ # Express backend (hand-built)
│ ├── node_modules/ # Installed dependencies (do NOT commit)
│ ├── index.js # Server entry point (the code above)
│ ├── package.json # Backend dependencies and scripts
│ └── package-lock.json # Pinned dependency versions
What was added in this lesson: Everything. Both project folders, both package.json files, all dependencies, and the taskpilot-server/index.js file.
Run the project you have built so far:
-
Start the backend: Open a terminal, navigate to
taskpilot-server/, and runnpm run dev. You should see:TaskPilot server listening on http://localhost:3001 Health check: http://localhost:3001/health -
Start the frontend: Open a second terminal, navigate to
taskpilot-client/, and runnpm run dev. You should see:VITE v5.x.x ready in XXX ms ➜ Local: http://localhost:5173/ -
Verify both are running: Open
http://localhost:3001in your browser — you should see the JSON response from Express. Openhttp://localhost:5173— you should see the Vite + React starter page with the spinning logo.
Congratulations — you now have a two-process full-stack application running on your machine. In Lesson 2, we will build real API routes into that Express server.
Checkpoint: Verify Your Understanding
Before moving on, make sure you can answer these questions confidently. If any feel shaky, re-read the relevant section.
🤔 What is the difference between dependencies and devDependencies in package.json?
View Answer
dependencies are packages your application needs to run in production (like Express — your server cannot function without it). devDependencies are packages only needed during development (like nodemon — your production server does not need auto-restart, it uses a process manager like PM2 instead). When you deploy to production and run npm install --production, only dependencies are installed, saving disk space and reducing attack surface.
🤔 Why do we use npm run dev instead of npm start during development?
View Answer
npm start runs node index.js, which starts the server once and does not watch for file changes. npm run dev runs nodemon index.js, which watches your files and automatically restarts the server whenever you save a change. During development, this automatic restart loop saves you from the tedious cycle of manually stopping and restarting your server after every edit.
🤔 Try modifying the code challenge: Open taskpilot-server/index.js and add a third route — app.get('/api/version', ...) — that returns { version: '1.0.0', name: 'TaskPilot' }. Save the file, watch nodemon restart, and visit http://localhost:3001/api/version to verify it works.
Hint: Click if you're stuck
Follow the exact pattern of the existing routes. The only things that change are the URL path (first argument to app.get) and the object you pass to res.json(). Express routes are independent — you can add as many as you want, and they do not interfere with each other.
Summary
| Concept | What It Does | When to Use | Watch Out For |
|---|---|---|---|
| Node.js | Runs JavaScript outside the browser | Every JS project — it is the runtime | Version mismatches between developers |
| npm | Installs and manages JavaScript packages | Every time you need a library | Never commit node_modules to Git |
package.json | Declares project identity and dependencies | Created once per project, updated as you add packages | Keep scripts section organized |
package-lock.json | Pins exact dependency versions | Automatically maintained by npm | Always commit this file to Git |
| Vite | Scaffolds and serves frontend projects | Starting new React/Vue/Svelte projects | Replaces deprecated Create React App |
| Express | Minimal web framework for Node.js | Building APIs and web servers | Does not include built-in ORM or auth |
| nodemon | Auto-restarts server on file changes | Development only — not for production | Install as devDependency, not dependency |
| VS Code + Extensions | Code editor with integrated tools | Every day of development | Enable format-on-save with Prettier immediately |
Next up: Lesson 2 — Your First Express Server. Both projects are running, and the workbench is solid. Now we dive deep into Express routing: HTTP methods (GET, POST, PUT, DELETE), request and response objects, middleware, and how to design API endpoints that follow REST conventions. By the end of Lesson 2, your backend will have a full set of task routes ready to accept data. The workbench is built — time to start building the furniture.
🟢 Too Easy? Here's your fast track.
Key takeaways to lock in: Node.js is the runtime, npm is the package manager, package.json is the manifest, and Vite is the modern frontend scaffolding tool. The frontend and backend are separate projects that communicate over HTTP.
Speed ahead: If you already have both projects running, try these before Lesson 2:
- Read through
taskpilot-client/vite.config.jsandtaskpilot-client/src/main.jsxto understand how Vite mounts your React app. - Add a
/api/tasksroute to your Express server that returns a hardcoded array of three task objects (each withid,title, andcompletedfields). This is exactly what we will build properly in Lesson 2.
🟡 Just Right? Reinforce with a different angle.
Think of your two projects as two separate businesses. taskpilot-client is a storefront that customers (users) visit. taskpilot-server is the warehouse that stores inventory (data) and fulfills orders (API requests). The storefront does not need to know how the warehouse is organized internally — it just sends orders and receives packages. This is the separation of concerns principle, and it is why we keep them in separate folders with separate dependencies. A change to the warehouse layout should never require remodeling the store.
Extra practice: Delete the node_modules folder from taskpilot-server (just the folder, not package.json or package-lock.json). Then run npm install. Everything should rebuild identically. This proves that node_modules is entirely reconstructable from your manifest files — which is why we never commit it to Git.
🔴 Challenge: Production-level thinking.
Scenario: You are deploying taskpilot-server to Railway (a cloud platform). Railway automatically sets the PORT environment variable and runs npm start to boot your application. But during deployment, npm install is also run — and it installs both dependencies and devDependencies by default, adding unnecessary packages like nodemon to your production container.
Your task:
- Research how to tell npm to skip
devDependenciesduring a production install (hint: there is a flag and an environment variable approach). - Add a
"engines"field to yourpackage.jsonthat specifies the Node.js version you are using. This tells Railway which version to use and prevents version mismatch bugs. - Modify your
/healthendpoint to also return the Node.js version (process.version) and the current environment (process.env.NODE_ENV || 'development'). Health endpoints that expose runtime information are standard practice in production systems — tools like Datadog and New Relic consume them for monitoring.
This challenge previews concepts we will cover in Lesson 12 (deployment), but solving it now will deepen your understanding of package.json and environment variables.