DEV Community

koushikmaya
koushikmaya

Posted on

week-07 Task-3 # package.json, SemVer, npm Scripts, Configuration & Monorepos: Understanding the Node.js Project Setup

When I first created a Node.js project, I mostly thought:

npm init
Enter fullscreen mode Exit fullscreen mode

creates a package.json, and then I install packages with:

npm install express
Enter fullscreen mode Exit fullscreen mode

Done.

But package.json is actually much more important than that.

It describes the project, manages dependencies, defines commands, and can even control how the application is started.

Then there are terms like:

  • SemVer
  • dependencies
  • devDependencies
  • npm scripts
  • environment variables
  • dotenv
  • configuration management
  • monorepos

At first, these felt like separate topics.

But they are all part of one bigger question:

How do we keep a Node.js project organized, reproducible, and configurable as it grows?


1. What Is package.json?

package.json is basically the identity card and configuration file of a Node.js project.

A simple example:

{
  "name": "my-node-app",
  "version": "1.0.0",
  "description": "A simple Node.js application",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js"
  },
  "dependencies": {
    "express": "^5.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

It tells npm things like:

  • What is this project called?
  • What version is it?
  • What packages does it need?
  • What commands can I run?
  • What is the entry point?
  • What kind of project is this?

2. Why Does package.json Matter?

Imagine giving your project to another developer.

You don't want to say:

"Install Express, install Jest, install this package, then run this command..."

Instead, they should be able to clone the project and run:

npm install
Enter fullscreen mode Exit fullscreen mode

npm reads:

package.json
Enter fullscreen mode Exit fullscreen mode

and knows what packages the project needs.

Then they can run:

npm start
Enter fullscreen mode Exit fullscreen mode

or:

npm test
Enter fullscreen mode Exit fullscreen mode

depending on the scripts defined by the project.

So package.json helps make a project repeatable.


3. Creating package.json

The usual command is:

npm init
Enter fullscreen mode Exit fullscreen mode

npm asks questions such as:

package name
version
description
entry point
test command
git repository
keywords
author
license
Enter fullscreen mode Exit fullscreen mode

You can also skip the interactive questions:

npm init -y
Enter fullscreen mode Exit fullscreen mode

This creates a default package.json.


4. Understanding the Main Fields

A common package.json might look like:

{
  "name": "node-api",
  "version": "1.0.0",
  "description": "A Node.js REST API",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "test": "node --test"
  },
  "dependencies": {
    "express": "^5.0.0"
  },
  "devDependencies": {
    "eslint": "^9.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's break this down.


5. name

"name": "node-api"
Enter fullscreen mode Exit fullscreen mode

This is the name of the package/project.

For a publishable npm package, naming rules matter.

For a normal application, it mainly identifies the project.


6. version

"version": "1.0.0"
Enter fullscreen mode Exit fullscreen mode

This represents the current version of the project/package.

This leads us to an important concept:

Semantic Versioning

or simply:

SemVer


7. SemVer: Semantic Versioning

Semantic Versioning usually looks like:

MAJOR.MINOR.PATCH
Enter fullscreen mode Exit fullscreen mode

For example:

2.4.7
Enter fullscreen mode Exit fullscreen mode

means:

MAJOR = 2
MINOR = 4
PATCH = 7
Enter fullscreen mode Exit fullscreen mode

The basic idea is:

2.4.7
│ │ │
│ │ └── PATCH
│ └──── MINOR
└────── MAJOR
Enter fullscreen mode Exit fullscreen mode

8. MAJOR Version

The major version generally changes when there are breaking changes.

For example:

1.5.2
Enter fullscreen mode Exit fullscreen mode

becomes:

2.0.0
Enter fullscreen mode Exit fullscreen mode

Imagine a library had:

getUser(id)
Enter fullscreen mode Exit fullscreen mode

and in version 2 it changes to:

getUser({ id })
Enter fullscreen mode Exit fullscreen mode

Existing code may stop working.

That's the kind of change that can justify a major version increase.

Think:

MAJOR = "Something may break."


9. MINOR Version

A minor version generally adds functionality while maintaining backward compatibility.

For example:

1.5.2
Enter fullscreen mode Exit fullscreen mode

becomes:

1.6.0
Enter fullscreen mode Exit fullscreen mode

Maybe a new feature is added:

getUser()
getUsers()
searchUsers()
Enter fullscreen mode Exit fullscreen mode

Existing code should continue working.

Think:

MINOR = "New functionality, but existing usage should continue working."


10. PATCH Version

A patch version is generally for backward-compatible fixes.

For example:

1.5.2
Enter fullscreen mode Exit fullscreen mode

becomes:

1.5.3
Enter fullscreen mode Exit fullscreen mode

Maybe a bug is fixed.

Think:

PATCH = "Bug fix."


11. The Easy SemVer Memory Trick

Remember:

MAJOR → Breaking changes
MINOR → New features
PATCH → Bug fixes
Enter fullscreen mode Exit fullscreen mode

So:

1.4.2
Enter fullscreen mode Exit fullscreen mode

can become:

2.0.0   → breaking change
1.5.0   → new feature
1.4.3   → bug fix
Enter fullscreen mode Exit fullscreen mode

This is the basic idea behind SemVer.


12. What Does ^ Mean?

You may have seen this:

"express": "^5.0.0"
Enter fullscreen mode Exit fullscreen mode

The ^ is a version range operator.

It tells npm that compatible updates within the allowed SemVer range can be installed.

For a dependency already using:

^5.0.0
Enter fullscreen mode Exit fullscreen mode

the range generally allows versions:

>=5.0.0 <6.0.0
Enter fullscreen mode Exit fullscreen mode

So npm can install a newer compatible release such as:

5.1.0
Enter fullscreen mode Exit fullscreen mode

or:

5.2.3
Enter fullscreen mode Exit fullscreen mode

but not:

6.0.0
Enter fullscreen mode Exit fullscreen mode

because that crosses the major version boundary.


13. What Does ~ Mean?

You may also see:

"express": "~5.0.0"
Enter fullscreen mode Exit fullscreen mode

The ~ is usually more restrictive.

It generally allows patch-level updates within the same minor version:

>=5.0.0 <5.1.0
Enter fullscreen mode Exit fullscreen mode

So versions such as:

5.0.1
5.0.2
5.0.9
Enter fullscreen mode Exit fullscreen mode

can fit the range, while:

5.1.0
Enter fullscreen mode Exit fullscreen mode

does not.


14. Exact Versions

You can also specify:

"express": "5.0.0"
Enter fullscreen mode Exit fullscreen mode

This requests exactly that version.

No SemVer range is being specified.

In practice, the lockfile also plays an important role in ensuring reproducible installations.


15. dependencies vs devDependencies

This is one of the most common Node.js questions.

What's the difference between:

"dependencies": {}
Enter fullscreen mode Exit fullscreen mode

and:

"devDependencies": {}
Enter fullscreen mode Exit fullscreen mode

The simple answer is:

dependencies are needed by the application.

devDependencies are mainly needed while developing, testing, building, or maintaining the application.


16. Example of a Dependency

Suppose we're building an Express API.

We write:

const express = require("express");
Enter fullscreen mode Exit fullscreen mode

Our application needs Express to actually run.

So:

npm install express
Enter fullscreen mode Exit fullscreen mode

will put Express under:

"dependencies"
Enter fullscreen mode Exit fullscreen mode

For example:

"dependencies": {
  "express": "^5.0.0"
}
Enter fullscreen mode Exit fullscreen mode

17. Example of a Dev Dependency

Suppose we use ESLint:

npm install --save-dev eslint
Enter fullscreen mode Exit fullscreen mode

ESLint helps us check our code.

But our production API doesn't need ESLint running to process:

GET /users
Enter fullscreen mode Exit fullscreen mode

It's a development tool.

So it belongs in:

"devDependencies"
Enter fullscreen mode Exit fullscreen mode

Example:

"devDependencies": {
  "eslint": "^9.0.0"
}
Enter fullscreen mode Exit fullscreen mode

18. More Examples

Usually dependencies

express
nestjs
mongoose
pg
drizzle-orm
jsonwebtoken
Enter fullscreen mode Exit fullscreen mode

These are packages your application may need while running.

Usually devDependencies

jest
eslint
prettier
typescript
nodemon
testing tools
build tools
Enter fullscreen mode Exit fullscreen mode

These are usually tools used during development, testing, or building.

The exact classification can depend on the project's architecture.


19. A Simple Question to Decide

When you're unsure where a package belongs, ask:

"Does my application need this package to actually run in its target environment?"

If yes:

dependencies
Enter fullscreen mode Exit fullscreen mode

If it's primarily there to help you develop, test, lint, format, or build:

devDependencies
Enter fullscreen mode Exit fullscreen mode

20. What Is package-lock.json?

When you install packages, npm usually creates:

package-lock.json
Enter fullscreen mode Exit fullscreen mode

This file records the exact dependency tree that npm resolved.

For example, package.json might say:

"express": "^5.0.0"
Enter fullscreen mode Exit fullscreen mode

That allows a range of versions.

But the lockfile records the specific versions selected during installation, along with their dependency information.

This helps different developers and CI environments install a consistent dependency tree.

So a useful mental model is:

package.json
→ What versions/ranges does the project allow?

package-lock.json
→ What exact dependency tree was resolved?
Enter fullscreen mode Exit fullscreen mode

For npm projects, the lockfile is normally committed to Git.


21. npm Scripts

Now let's talk about another extremely useful part of package.json:

"scripts": {}
Enter fullscreen mode Exit fullscreen mode

Scripts are shortcuts for commands we repeatedly run.

For example:

"scripts": {
  "start": "node src/index.js",
  "test": "node --test"
}
Enter fullscreen mode Exit fullscreen mode

Now instead of typing:

node src/index.js
Enter fullscreen mode Exit fullscreen mode

we can type:

npm start
Enter fullscreen mode Exit fullscreen mode

And:

npm test
Enter fullscreen mode Exit fullscreen mode

runs the test command.


22. Why Are npm Scripts Useful?

Imagine a project has these commands:

node src/index.js
npm run lint
npm test
npm run build
Enter fullscreen mode Exit fullscreen mode

Instead of asking every developer to memorize them, we put them in:

"scripts"
Enter fullscreen mode Exit fullscreen mode

Then the project becomes self-documenting.

For example:

"scripts": {
  "dev": "node --watch src/index.js",
  "start": "node src/index.js",
  "test": "node --test",
  "lint": "eslint ."
}
Enter fullscreen mode Exit fullscreen mode

Now I know:

npm run dev
npm start
npm test
npm run lint
Enter fullscreen mode Exit fullscreen mode

23. npm run

For most custom scripts, use:

npm run <script>
Enter fullscreen mode Exit fullscreen mode

For example:

npm run dev
Enter fullscreen mode Exit fullscreen mode
npm run lint
Enter fullscreen mode Exit fullscreen mode
npm run build
Enter fullscreen mode Exit fullscreen mode

Some scripts have special npm shortcuts.

For example:

npm start
npm test
Enter fullscreen mode Exit fullscreen mode

are commonly used directly.


24. A Realistic scripts Section

A Node.js project might have:

"scripts": {
  "dev": "node --watch src/index.js",
  "start": "node src/index.js",
  "test": "node --test",
  "lint": "eslint .",
  "format": "prettier --write ."
}
Enter fullscreen mode Exit fullscreen mode

This gives the project a predictable interface.

As a developer, I don't need to remember exactly how every command works internally.

I just run the project's scripts.


25. Configuration Management

Now let's imagine our application needs:

PORT
DATABASE_URL
JWT_SECRET
API_KEY
NODE_ENV
Enter fullscreen mode Exit fullscreen mode

We could hard-code them:

const port = 3000;
const databaseUrl = "postgres://...";
Enter fullscreen mode Exit fullscreen mode

But this is a bad idea for many real applications.

Why?

Because different environments need different values.

For example:

Development
PORT=3000

Testing
PORT=4000

Production
PORT=8080
Enter fullscreen mode Exit fullscreen mode

The same code should ideally work in all three environments.

Only the configuration should change.


26. Environment Variables

Environment variables let us keep configuration outside the source code.

For example:

PORT=3000
NODE_ENV=development
DATABASE_URL=postgres://localhost/mydb
Enter fullscreen mode Exit fullscreen mode

Then Node.js can access them using:

process.env.PORT
Enter fullscreen mode Exit fullscreen mode

For example:

const port = Number(process.env.PORT || 3000);
Enter fullscreen mode Exit fullscreen mode

This means:

Use the PORT environment variable if it exists; otherwise use 3000.


27. Why Not Put Secrets in Git?

Imagine this:

const JWT_SECRET = "super-secret-password";
Enter fullscreen mode Exit fullscreen mode

and then we push the code to GitHub.

Now the secret may be exposed.

Instead, we can use:

JWT_SECRET=some-secret-value
Enter fullscreen mode Exit fullscreen mode

and read it through:

process.env.JWT_SECRET
Enter fullscreen mode Exit fullscreen mode

The actual secret should be provided through the deployment environment or a secret-management system.


28. What Is dotenv?

Node.js can read environment variables supplied by the operating system through:

process.env
Enter fullscreen mode Exit fullscreen mode

But during local development, it is convenient to store variables in a .env file.

For example:

PORT=3000
NODE_ENV=development
DATABASE_URL=postgres://localhost/social_feed_dev
Enter fullscreen mode Exit fullscreen mode

A popular package called dotenv can load these values into process.env.

Install it:

npm install dotenv
Enter fullscreen mode Exit fullscreen mode

Then:

require("dotenv").config();
Enter fullscreen mode Exit fullscreen mode

Now:

console.log(process.env.PORT);
Enter fullscreen mode Exit fullscreen mode

can access the value from .env.


29. dotenv/config

There is also a convenient way to load dotenv automatically.

For CommonJS:

require("dotenv").config();
Enter fullscreen mode Exit fullscreen mode

For some Node.js setups, you may see:

node -r dotenv/config src/index.js
Enter fullscreen mode Exit fullscreen mode

The idea is:

Load the .env configuration before the application starts.

The exact approach depends on how the project is structured.


30. .env Should Usually Not Be Committed

A typical project might have:

.env
.env.example
Enter fullscreen mode Exit fullscreen mode

.env contains actual local values:

DATABASE_URL=real-local-value
JWT_SECRET=real-secret
Enter fullscreen mode Exit fullscreen mode

.env.example contains the expected variable names without real secrets:

PORT=
NODE_ENV=
DATABASE_URL=
JWT_SECRET=
Enter fullscreen mode Exit fullscreen mode

Then .gitignore should normally contain:

.env
Enter fullscreen mode Exit fullscreen mode

This gives other developers a template without exposing your secrets.


31. Configuration Should Be Centralized

Instead of scattering:

process.env.PORT
process.env.DATABASE_URL
process.env.JWT_SECRET
Enter fullscreen mode Exit fullscreen mode

throughout the application, we can create a configuration layer.

For example:

const config = {
  port: Number(process.env.PORT || 3000),
  databaseUrl: process.env.DATABASE_URL,
  jwtSecret: process.env.JWT_SECRET
};

module.exports = config;
Enter fullscreen mode Exit fullscreen mode

Then other parts of the application can use:

config.port
Enter fullscreen mode Exit fullscreen mode

instead of directly reading environment variables everywhere.

This creates a clean boundary:

Environment
     ↓
Configuration
     ↓
Application
Enter fullscreen mode Exit fullscreen mode

32. Configuration Validation

There's another important improvement.

Suppose:

DATABASE_URL
Enter fullscreen mode Exit fullscreen mode

is missing.

Our application might start and fail much later with a confusing database error.

Instead, we can validate configuration at startup.

Conceptually:

Application starts
       ↓
Load configuration
       ↓
Validate required values
       ↓
Invalid?
  ├── Yes → Fail fast
  └── No  → Start application
Enter fullscreen mode Exit fullscreen mode

For example:

if (!process.env.DATABASE_URL) {
  throw new Error("DATABASE_URL is required");
}
Enter fullscreen mode Exit fullscreen mode

In larger applications, configuration libraries can make this cleaner.


33. Development vs Production Configuration

The same application may run in:

Development
Testing
Staging
Production
Enter fullscreen mode Exit fullscreen mode

For example:

NODE_ENV=development
Enter fullscreen mode Exit fullscreen mode

or:

NODE_ENV=production
Enter fullscreen mode Exit fullscreen mode

Configuration may differ between these environments.

But the application code should ideally remain the same.

Think:

Same application
       +
Different configuration
       =
Different environment
Enter fullscreen mode Exit fullscreen mode

34. What Is a Monorepo?

Now let's move to a bigger project concept.

A monorepo means keeping multiple related projects/packages in one Git repository.

For example:

company-project/
│
├── apps/
│   ├── web/
│   ├── api/
│   └── admin/
│
├── packages/
│   ├── ui/
│   ├── config/
│   └── shared/
│
└── package.json
Enter fullscreen mode Exit fullscreen mode

Instead of having:

frontend-repo
backend-repo
shared-repo
Enter fullscreen mode Exit fullscreen mode

we keep them together.


35. Why Use a Monorepo?

Imagine a company has:

React Web App
Node API
Admin Dashboard
Shared UI Components
Shared TypeScript Types
Enter fullscreen mode Exit fullscreen mode

These projects may depend on each other.

A monorepo makes it easier to manage them together.

For example:

apps/web
      ↓
packages/ui

apps/api
      ↓
packages/types
Enter fullscreen mode Exit fullscreen mode

The shared packages live in the same repository.


36. Monorepo vs Polyrepo

Polyrepo

Separate repositories:

frontend-repo
backend-repo
mobile-repo
shared-types-repo
Enter fullscreen mode Exit fullscreen mode

Monorepo

One repository:

company-repo
├── apps
│   ├── frontend
│   ├── backend
│   └── mobile
└── packages
    └── shared
Enter fullscreen mode Exit fullscreen mode

Neither approach is automatically better.

The right choice depends on the team's needs, deployment strategy, tooling, and project structure.


37. npm Workspaces

npm itself supports a basic monorepo mechanism called workspaces.

A root package.json might contain:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Then the repository can contain:

apps/
  api/
  web/

packages/
  shared/
  ui/
Enter fullscreen mode Exit fullscreen mode

Each workspace can have its own:

package.json
Enter fullscreen mode Exit fullscreen mode

while npm manages the overall workspace.


38. Why private: true?

You will often see:

"private": true
Enter fullscreen mode Exit fullscreen mode

in the root package of a monorepo.

It tells npm that this package is not intended to be published as a normal npm package.

The root is mainly being used to manage the workspace.


39. Monorepo Mental Model

Think of a monorepo as a company building.

              Company Building
                    |
       ┌────────────┼────────────┐
       |            |            |
     Web App      API        Admin App
       |            |            |
       └────────────┼────────────┘
                    |
              Shared Packages
Enter fullscreen mode Exit fullscreen mode

Everything is in one repository, but each application/package can still have its own responsibility.


40. Do I Need a Monorepo as a Beginner?

Probably not.

If you're building:

one Node.js API
Enter fullscreen mode Exit fullscreen mode

a normal repository is perfectly fine:

my-api/
├── src/
├── test/
├── package.json
├── package-lock.json
└── .env.example
Enter fullscreen mode Exit fullscreen mode

Don't introduce monorepo tooling just because it sounds advanced.

First understand:

  • npm
  • package.json
  • dependencies
  • scripts
  • configuration
  • Git
  • testing
  • application structure

Then learn monorepos when your project actually has multiple related applications/packages.


41. Putting Everything Together

A realistic Node.js project might look like:

my-api/
│
├── src/
│   ├── config/
│   │   └── index.js
│   ├── routes/
│   ├── services/
│   └── index.js
│
├── test/
│
├── .env
├── .env.example
├── .gitignore
├── package.json
├── package-lock.json
└── README.md
Enter fullscreen mode Exit fullscreen mode

The package.json might look like:

{
  "name": "my-api",
  "version": "1.0.0",
  "scripts": {
    "dev": "node --watch src/index.js",
    "start": "node src/index.js",
    "test": "node --test"
  },
  "dependencies": {
    "dotenv": "^17.0.0",
    "express": "^5.0.0"
  },
  "devDependencies": {
    "eslint": "^9.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

And .env.example:

PORT=3000
NODE_ENV=development
DATABASE_URL=
JWT_SECRET=
Enter fullscreen mode Exit fullscreen mode

Now the project has:

package.json
    ↓
Project metadata + dependencies + scripts

package-lock.json
    ↓
Resolved dependency tree

.env
    ↓
Local configuration

dotenv
    ↓
Loads local environment variables

npm scripts
    ↓
Standard project commands
Enter fullscreen mode Exit fullscreen mode

42. A Simple Mental Model

If I had to remember the whole topic in one diagram:

                    Node.js Project
                          |
            ┌─────────────┼─────────────┐
            |             |             |
       package.json     .env        package-lock
            |             |             |
       ┌────┴────┐        |        Exact dependency
       |         |        |           tree
 dependencies  scripts    |
       |         |        |
   Runtime    Commands   Configuration
   packages              |
                          ↓
                    process.env
Enter fullscreen mode Exit fullscreen mode

And when the project becomes large:

                 Monorepo
                    |
          ┌─────────┴─────────┐
          |                   |
        apps/              packages/
          |                   |
      web / api          shared / ui
Enter fullscreen mode Exit fullscreen mode

43. Quick Revision Cheat Sheet

Concept Simple Meaning
package.json Project metadata, dependencies, scripts, and configuration
version Current package/project version
SemVer Versioning convention: MAJOR.MINOR.PATCH
MAJOR Breaking changes
MINOR Backward-compatible features
PATCH Backward-compatible fixes
^ Allows compatible updates within a major version
~ Usually allows patch updates within a minor version
dependencies Packages needed by the application
devDependencies Packages mainly needed for development/testing/building
package-lock.json Records the resolved dependency tree
npm scripts Named commands defined in package.json
Environment variable Configuration supplied outside the code
process.env Accesses environment variables in Node.js
dotenv Loads variables from .env into process.env
.env Local configuration/secrets; normally not committed
.env.example Template showing required variables
Configuration layer Central place for application configuration
Monorepo Multiple related projects/packages in one repository
npm workspaces npm's built-in workspace/monorepo mechanism

44. Final Takeaway

The biggest lesson for me is that a Node.js project isn't just:

JavaScript files
Enter fullscreen mode Exit fullscreen mode

There is an entire ecosystem around the application.

package.json
     ↓
Defines the project
     ↓
Dependencies + scripts
     ↓
Application runs
     ↓
Configuration comes from environment
     ↓
dotenv can load local .env values
     ↓
package-lock keeps installations reproducible
Enter fullscreen mode Exit fullscreen mode

And when the project grows:

One project
    ↓
Multiple applications/packages
    ↓
Monorepo
Enter fullscreen mode Exit fullscreen mode

The important thing isn't memorizing every npm command.

It's understanding why these files and concepts exist.

Once that is clear, commands like:

npm install
npm install --save-dev eslint
npm run dev
npm test
npm start
Enter fullscreen mode Exit fullscreen mode

stop feeling like random commands.

They become tools for managing a real software project.

And that's the point where Node.js development starts feeling much more organized.

Top comments (0)