Course:Node.js & Express/
Lesson

Every Node.js project revolves around a single JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. file: package.json. It is the project's ID card, it tells npm what the project is called, what version it is on, which packages it depends on, and what commands are available to run. Understanding this file well will save you from dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. confusion, broken deployments, and "it works on my machine" debugging sessions.

Creating a project from scratch

The npm init command walks you through setting up package.json. Adding the -y flag accepts all defaults immediately:

mkdir my-api
cd my-api
npm init -y

The result is a minimal package.json:

json
{
  "name": "my-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

You will customise most of these fields before pushing to production, but this is a perfectly valid starting point.

02

Important fields explained

name and version

The name field must be all lowercase with no spaces (hyphens are fine). It only needs to be globally unique if you plan to publish the package to the npm registryWhat is registry?A server that stores and distributes packages or container images - npm registry for JavaScript packages, Docker Hub for container images.. For private applications, it just needs to be recognisable.

The version field follows semantic versioningWhat is semantic versioning?A numbering system (major.minor.patch) that communicates whether a release contains breaking changes, new features, or bug fixes. (semver): MAJOR.MINOR.PATCH. Increment MAJOR for breaking changes, MINOR for new backwards-compatible features, and PATCH for bug fixes.

main

Tells Node.js which file to load when someone does require('your-package') or import from it. For a library this matters a lot. For an application it is less critical but still good practice to keep it accurate.

type: moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. vs CommonJSWhat is commonjs?Node.js's original module format using require() and module.exports - distinct from and predating the ES module import/export syntax.

By default, Node.js treats .js files as CommonJS modules (require / module.exports). Add "type": "module" to switch to ES modulesWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json. (import / export) across all .js files in the project:

json
{
  "type": "module"
}
You can mix both systems in one project by using .mjs for ES module files and .cjs for CommonJS files, regardless of the type field. But choosing one style and sticking to it is much simpler.

engines

Declares which versions of Node.js and npm your project requires. This is advisory by default, but tools like Heroku and certain CI systems enforce it:

json
{
  "engines": {
    "node": ">=20.0.0",
    "npm": ">=10.0.0"
  }
}

private

Setting "private": true prevents you from accidentally running npm publish and leaking an internal application to the public registry. Always include this for applications (as opposed to libraries):

json
{
  "private": true
}
03

Installing dependencies

# A production dependency - added to "dependencies"
npm install express

# A dev-only dependency - added to "devDependencies"
npm install --save-dev nodemon

# Multiple packages at once
npm install express lodash dotenv
npm install --save-dev jest eslint

After running these commands, the packages appear in node_modules/ and the version ranges are recorded in package.json.

04

Dependencies vs devDependencies

json
{
  "dependencies": {
    "express": "^4.18.0",
    "dotenv":  "^16.0.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.0",
    "jest":    "^29.0.0",
    "eslint":  "^8.0.0"
  }
}

When you deploy to a server and run npm install --omit=dev (or the older npm install --production), npm only installs dependencies. This keeps the production bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together. lean and avoids shipping test tools to your server.

The line between the two is a business rule, not a technical enforcement. A package in dependencies works the same way as one in devDependencies, it is just a signal about when it is needed.
05

Semantic versioningWhat is semantic versioning?A numbering system (major.minor.patch) that communicates whether a release contains breaking changes, new features, or bug fixes. ranges

When npm updates your packages, it uses the version prefix to decide which releases are acceptable:

PrefixRange allowedExample
1.2.3 (no prefix)Exactly that versionOnly 1.2.3
^1.2.3Minor and patch updates>=1.2.3 <2.0.0
~1.2.3Patch updates only>=1.2.3 <1.3.0
>=1.2.0Anything at or above1.2.0, 2.0.0, …
*Any versionLatest available

The caret (^) is the npm default. It allows new features (minor) and bug fixes (patch) but blocks breaking changes (major). For most packages this is a reasonable policy. For packages with a history of breaking minor versions, pin to an exact version instead.

06

Scripts

The scripts field is a map of short names to shell commands. Using it means your whole team runs tasks the same way, regardless of which tools are installed globally on their machines:

json
{
  "scripts": {
    "start":       "node src/index.js",
    "dev":         "nodemon src/index.js",
    "test":        "jest",
    "test:watch":  "jest --watch",
    "lint":        "eslint src/",
    "build":       "tsc",
    "prebuild":    "npm run lint",
    "postbuild":   "echo 'Build complete'"
  }
}
npm start         # No 'run' needed for 'start'
npm test          # No 'run' needed for 'test'
npm run dev       # Custom scripts need 'run'
npm run lint
npm run build     # Runs 'prebuild' first, then 'build', then 'postbuild'

Hooks with the pre and post prefix run automatically before and after the matching script. This is useful for enforcing linting before every build.

07

The package-lock.jsonWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. file

Every time you run npm install, npm also writes (or updates) package-lock.json. This file records the exact version of every package in your dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. tree, not just the top-level ones, but every package those packages depend on.

Why does this matter? The version ranges in package.json (like ^4.18.0) allow updates. Two developers running npm install on different days might get different minor versions. package-lock.json eliminates that variability by pinning everything exactly.

Always commitWhat is commit?A permanent snapshot of your staged changes saved in Git's history, identified by a unique hash and accompanied by a message describing what changed. package-lock.json. Never commit node_modules/.

If package-lock.json ever gets into a confused state, you can safely delete it and run npm install to regenerate it from scratch. The result will install the latest allowed versions of all packages.
08

Project structure best practices

my-api/
├── package.json          ← committed
├── package-lock.json     ← committed
├── .gitignore            ← committed
├── .nvmrc                ← committed
├── src/
│   ├── index.js
│   ├── routes/
│   ├── middleware/
│   └── utils/
├── tests/
└── node_modules/NEVER committed

A solid .gitignore for a Node.js project looks like this:

gitignore
# Dependencies - always large, always regeneratable
node_modules/

# Logs
*.log
npm-debug.log*

# Environment secrets
.env
.env.local
.env.*.local

# Build output
dist/
build/

# OS noise
.DS_Store
Thumbs.db
09

Quick reference

CommandWhat it does
npm init -yCreate package.json with defaults
npm install expressAdd to dependencies
npm install -D jestAdd to devDependencies
npm installInstall all dependencies from package.json
npm install --omit=devInstall production dependencies only
npm run devRun the dev script
npm startRun the start script (no run needed)
npm updateUpdate packages within their allowed ranges