Create your own
Lesson illustration

Managing Dependencies and Commands with npm Scripts

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:

ItemRoleCommit to Git?
package.jsonHuman-maintained project manifest: metadata, direct dependencies, and scriptsYes
package-lock.jsonExact dependency tree resolved by npmYes
node_modules/Downloaded packages available on this machineNo

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 VS Code project with `package.json` open. The file records project metadata, scripts, and installed direct dependencies; the Explorer also shows the generated `node_modules` folder and `package-lock.json`.

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:

  • name identifies the project/package.
  • version is the project’s release version. It matters especially if you publish a reusable package.
  • private: true protects an application from accidental publication to the public npm registry.
  • scripts defines named terminal commands.
  • dependencies and devDependencies are 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:

  • mongoose when the running API accesses MongoDB,
  • a request-validation library used by API routes,
  • a password-hashing library used during sign-up or login,
  • cors when 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 purposeExampleSection
Required to handle a real API request in productionexpressdependencies
Required for MongoDB access in the deployed APImongoosedependencies
Required only to restart a local server on editsnodemondevDependencies
Required only for linting or testseslint, test toolsdevDependencies
Required to build the frontend bundleviteusually 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:

  • 5 is the major version: potentially breaking changes.
  • 1 is the minor version: backward-compatible features.
  • 0 is 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:

ScriptIntent
devStart local development with automatic restart
startStart the application normally
testRun automated tests
lintCheck code quality rules
buildProduce deployable output, especially for a frontend
checkRun 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.json and package-lock.json disagree.
  • It removes existing node_modules before 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 run lists the available scripts.
  • npm ls displays installed dependency relationships and can reveal invalid or missing packages.
  • npm outdated compares 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.

  1. In the project root, confirm the starting state:

    npm run
    

    If there is no package.json, initialize one:

    npm init -y
    
  2. Ensure Express is recorded as a runtime dependency. If the project does not already use it:

    npm install express
    
  3. Install Nodemon as a development dependency:

    npm install -D nodemon
    
  4. Open package.json and set the scripts. Adjust src/server.js if 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.

  5. Run local development mode:

    npm run dev
    

    Change a harmless console.log statement or route response in the server file, save it, and confirm Nodemon restarts the process.

  6. Stop the process with Control+C, then run the normal start script:

    npm start
    

    This time, change and save the file. Node should not automatically restart. That distinction is intentional: dev is optimized for local iteration, while start runs the application plainly.

  7. Inspect the dependency changes:

    npm ls --depth=0
    

    Confirm that Express is listed as a top-level production dependency and Nodemon as a top-level development dependency in package.json.

  8. Finally, check Git status. A healthy project change normally includes:

    package.json
    package-lock.json
    

    It should not include node_modules/. If it does, add node_modules/ to .gitignore.


Key takeaways

  • package.json declares the project’s metadata, direct dependencies, and shared commands.
  • dependencies are needed when the deployed application runs; devDependencies support 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.json and package-lock.json, but never commit node_modules.
  • Use npm install for ordinary local setup and npm ci for strict, clean, lockfile-based installation.
  • npm scripts turn raw terminal commands into a small, documented interface for everyone working on the repository.
  • npm start and npm test are shortcuts; custom scripts run through npm 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