Good project workflow is what turns “it works on my machine” into software another developer can clone, install, and run predictably. In the previous lesson, you inspected browser requests in DevTools to see exactly what your frontend and API exchanged. Now we shift to the project root: the files and commands that define the tools your application needs and the standard ways to run them.
By the end of this lesson, you should be able to initialize an npm project, decide whether a package belongs in dependencies or devDependencies, understand the relationship between package.json, package-lock.json, and node_modules, and create scripts such as npm run dev and npm start. This is the workflow foundation you will use when creating React applications with Vite in the next module.
The three files and folders that define an npm project
npm is Node.js’s package manager and command-line tool. Its core job is to download third-party packages and record the project’s requirements so the same project can be recreated elsewhere.
At the root of a typical Express project, you will see:
task-api/
src/
server.js
package.json
package-lock.json
node_modules/
Each has a different job:
| Item | Role | Commit to Git? |
|---|---|---|
package.json | Human-maintained project manifest: metadata, direct dependencies, and scripts | Yes |
package-lock.json | Exact dependency tree resolved by npm | Yes |
node_modules/ | Downloaded packages available on this machine | No |
Think of package.json as the project’s declared contract. It says, for example, “this API needs Express, and developers need a watcher while working locally.”
node_modules is the installed result of that contract. It can contain hundreds or thousands of folders, including packages that your direct dependencies require. These indirect packages are called transitive dependencies. You do not commit node_modules because any developer or deployment environment can regenerate it.
package-lock.json records the exact versions npm selected for both direct and transitive dependencies. It prevents a common team problem: one machine receives a newly released compatible package version while another receives an older one.

A minimal package.json generated by npm might resemble this:
{
"name": "task-api",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nodemon src/server.js",
"start": "node src/server.js"
}
}
A few fields matter immediately:
nameidentifies the project/package.versionis the project’s release version. It matters especially if you publish a reusable package.private: trueprotects an application from accidental publication to the public npm registry.scriptsdefines named terminal commands.dependenciesanddevDependenciesare added when you install packages.
One field you may see is "main": "index.js". Do not confuse it with a start command. main describes the entry module when another project imports this package. It does not make Node automatically run that file. For an application, npm start is controlled by the "start" script.
Install dependencies intentionally
A dependency is code supplied by another package. In a full-stack JavaScript project, dependencies may include Express, database libraries, validation packages, React, build tooling, linters, and test frameworks.
The central classification question is:
Does the application need this package while it is running in production, or only while developers build, check, or test it?
Specifying dependencies and devDependencies in a package.json file | npm Docs
Read the npm documentation’s explanation of dependency categories and the commands npm uses to save them. This is the official reference for the distinction you will apply in the lab.
On the npm Docs page, begin with the opening explanation beneath the title and read through the subsection “Adding dependencies to a package.json file from the command line.” Focus on dependency classification, then note that ordinary npm install saves a production dependency while npm install <package> --save-dev saves a development-only one.
Runtime dependencies
A package belongs in "dependencies" when the deployed application imports or requires it while serving users.
For an Express API:
npm install express
npm downloads Express, adds it to node_modules, and writes an entry under "dependencies" in package.json.
Other typical server-side runtime dependencies include:
mongoosewhen the running API accesses MongoDB,- a request-validation library used by API routes,
- a password-hashing library used during sign-up or login,
corswhen the server configures browser cross-origin access.
Development dependencies
A package belongs in "devDependencies" when it helps developers build, test, inspect, or maintain code but is not imported by the running production server.
For example:
npm install --save-dev nodemon
or its short form:
npm install -D nodemon
nodemon restarts a local Node server when source files change. It is useful during development; the production server should run Node directly under its deployment process manager or container configuration.
Common development dependencies include:
- ESLint and Prettier,
- test runners and test utilities,
- TypeScript and type definitions,
- Vite and its React plugin in a React application,
- development watchers such as Nodemon.
Here is the classification in a compact form:
| Package purpose | Example | Section |
|---|---|---|
| Required to handle a real API request in production | express | dependencies |
| Required for MongoDB access in the deployed API | mongoose | dependencies |
| Required only to restart a local server on edits | nodemon | devDependencies |
| Required only for linting or tests | eslint, test tools | devDependencies |
| Required to build the frontend bundle | vite | usually devDependencies |
The last row deserves care. Vite is not needed by a browser after you deploy a built React site, but it is needed in the build environment that creates the production bundle. A deployment pipeline therefore must install dev dependencies during its build stage, even though the final static frontend does not run Vite.
Do not edit dependency versions casually
You can type dependency entries manually into package.json, but installing through npm is safer for normal work because npm updates both manifest and lockfile consistently.
Use these commands deliberately:
npm install express
npm install -D nodemon
npm uninstall nodemon
npm install express@5.1.0
The final command illustrates installing a specific version. The version you choose should come from a compatibility or security decision, not a guess.
A version such as "^5.1.0" uses semantic versioning ranges:
5is the major version: potentially breaking changes.1is the minor version: backward-compatible features.0is the patch version: backward-compatible fixes.- The caret (
^) commonly permits compatible updates below the next major version.
The lockfile then pins the exact version actually installed for the project. This combination gives flexibility when intentionally updating dependencies while preserving reproducibility for normal installs.
Scripts are the project’s shared command interface
Without scripts, each developer has to remember and type raw commands such as:
nodemon src/server.js
That seems trivial in a small project. It becomes error-prone when commands need flags, environment settings, test setup, code generation, or multiple tools.
An npm script stores a command behind a stable project-level name:
{
"scripts": {
"dev": "nodemon src/server.js",
"start": "node src/server.js",
"test": "node --test",
"check": "npm run lint && npm test"
}
}
You run the commands with:
npm run dev
npm start
npm test
npm run check
start and test have convenient shortcuts, so npm start and npm test work without writing run. For custom script names such as dev, lint, build, or check, use npm run <name>.
Importantly, npm scripts automatically make executables from local packages available. If Nodemon is installed locally, your script can use nodemon directly even if you never installed Nodemon globally. This avoids machine-specific global tool versions.
Node.js Tutorial - 56 - npm Scripts
Watch Codevolution’s “Node.js Tutorial - 56 - npm Scripts” for a compact demonstration of why scripts belong in package.json and how a start script is executed.
Watch the overview for the purpose of project-level scripts. Then watch the start example to see a script defined and run from the terminal. Finish with the shortcut to confirm why npm start is equivalent to npm run start.
Script names should communicate intent
Use names based on what a contributor wants to accomplish, not on the internal command detail.
A professional baseline for an API often looks like this:
| Script | Intent |
|---|---|
dev | Start local development with automatic restart |
start | Start the application normally |
test | Run automated tests |
lint | Check code quality rules |
build | Produce deployable output, especially for a frontend |
check | Run a fast combined quality gate, such as linting and tests |
For a future Vite React project, you will usually find scripts close to:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
You do not need to memorize Vite commands because package.json documents them. Your responsibility is to read the scripts before running a project and understand what each one does.
Repeatable installation: npm install and npm ci
When you clone a Node project, do not copy another developer’s node_modules directory. From the folder containing package.json, install the project requirements.
For normal local work:
npm install
npm reads package.json, uses package-lock.json when present, and creates or updates node_modules. It may update the lockfile if the manifest and lockfile need reconciliation.
For continuous integration and reproducible clean environments:
npm ci
npm ci is stricter:
- It requires a
package-lock.json. - It installs the exact locked dependency tree.
- It fails if
package.jsonandpackage-lock.jsondisagree. - It removes existing
node_modulesbefore installing.
That makes npm ci a strong default in CI pipelines, automated tests, and clean deployment builds. It exposes an uncommitted lockfile change instead of silently proceeding with a different dependency tree.
In a production environment that only runs an already built Node server, a typical install command is:
npm ci --omit=dev
This omits development dependencies. As noted earlier, do this only after any build stage that needs tools such as Vite or TypeScript.
Inspect before “fixing” packages
A few useful inspection commands are:
npm run
npm ls
npm outdated
npm runlists the available scripts.npm lsdisplays installed dependency relationships and can reveal invalid or missing packages.npm outdatedcompares installed versions with versions allowed by your declared ranges.
Treat update and security commands as review tools, not magic repairs. In particular, do not run forceful automatic upgrades blindly on a working project. Read the proposed changes, update intentionally, run tests, and commit package.json and package-lock.json together.
Also remember that an npm install can execute package lifecycle scripts. Install packages from trusted, correctly spelled sources, review additions in version control, and avoid exposing .npmrc tokens or environment secrets.
Hands-on: add a repeatable development workflow
Use an existing Express project from your middleware practice, or create a small scratch folder. This should take about 12–15 minutes.
-
In the project root, confirm the starting state:
npm runIf there is no
package.json, initialize one:npm init -y -
Ensure Express is recorded as a runtime dependency. If the project does not already use it:
npm install express -
Install Nodemon as a development dependency:
npm install -D nodemon -
Open
package.jsonand set the scripts. Adjustsrc/server.jsif your actual entry file has a different path:{ "scripts": { "dev": "nodemon src/server.js", "start": "node src/server.js" } }Keep any useful existing scripts, such as
test; add these entries rather than overwriting the whole file. -
Run local development mode:
npm run devChange a harmless
console.logstatement or route response in the server file, save it, and confirm Nodemon restarts the process. -
Stop the process with
Control+C, then run the normal start script:npm startThis time, change and save the file. Node should not automatically restart. That distinction is intentional:
devis optimized for local iteration, whilestartruns the application plainly. -
Inspect the dependency changes:
npm ls --depth=0Confirm that Express is listed as a top-level production dependency and Nodemon as a top-level development dependency in
package.json. -
Finally, check Git status. A healthy project change normally includes:
package.json package-lock.jsonIt should not include
node_modules/. If it does, addnode_modules/to.gitignore.
Key takeaways
package.jsondeclares the project’s metadata, direct dependencies, and shared commands.dependenciesare needed when the deployed application runs;devDependenciessupport development, builds, linting, or tests.npm install <package>saves a runtime dependency by default;npm install -D <package>saves a development dependency.- Commit both
package.jsonandpackage-lock.json, but never commitnode_modules. - Use
npm installfor ordinary local setup andnpm cifor strict, clean, lockfile-based installation. - npm scripts turn raw terminal commands into a small, documented interface for everyone working on the repository.
npm startandnpm testare shortcuts; custom scripts run throughnpm run <script-name>.
You have now completed the JavaScript workflow bridge for the course. Next, you will begin React by creating and running a React application with Vite—where dependency management and npm scripts immediately become part of your everyday development loop.
Can't find a good explanation? Sign up and we'll make it for you
Sign up