Understanding Package.json and Dependencies
Learn how npm works, what package.json controls, how to install and manage dependencies, and why lock files matter for reproducible builds.
What package.json actually does
Every Node.js project has a package.json file at its root. Think of it as the project's identity card and instruction manual combined. It tells other developers (and tools) what the project is called, what version it is, what it depends on, and how to run it. When you clone a project from GitHub and run npm install, npm reads package.json to know exactly which packages to download. Without it, you would need to manually track every library your project uses — and every library those libraries use. Open your terminal and navigate to an empty folder. Run: npm init -y. This creates a package.json with sensible defaults. Open the file and you will see fields like name, version, description, main, and scripts. The name field is your project's identifier on npm if you ever publish it. The version field follows semantic versioning: major.minor.patch. The main field tells Node.js which file to run when someone imports your package. Ask Claude Code: Explain each field in my package.json and suggest improvements. Claude Code will read the file, explain what each field controls, and recommend additions like a description, keywords, and a license field. These metadata fields matter more than you think — they affect how your project appears in npm search results, how other developers understand your project, and how tools like bundlers resolve your code.
Installing and managing packages
The npm registry hosts over two million packages — reusable code libraries that solve common problems. Instead of writing your own date formatting, HTTP client, or validation logic, you install a package that does it. Run: npm install dayjs. This does three things: downloads the dayjs package into a node_modules folder, adds dayjs to the dependencies section of package.json, and updates package-lock.json with the exact version installed. The node_modules folder can be enormous — thousands of files for even small projects. Never commit it to git. Add node_modules to your .gitignore file. There are two types of dependencies. Regular dependencies (npm install express) are packages your project needs to run in production. Dev dependencies (npm install --save-dev jest) are packages needed only during development — testing frameworks, linters, build tools. Ask Claude Code: Look at my package.json and tell me if any dependencies should be moved to devDependencies. This distinction matters for production deployments where you want to install only what is necessary. To remove a package: npm uninstall dayjs. To update all packages to their latest compatible versions: npm update. To check for outdated packages: npm outdated. This shows a table of current versus wanted versus latest versions. Ask Claude Code: Check my dependencies for security vulnerabilities and outdated packages, then suggest which ones to update. Claude Code will run npm audit and npm outdated, interpret the results, and recommend specific actions.
Understanding version ranges and semver
In package.json, you will see version numbers with symbols like ^ and ~. These control which versions npm is allowed to install. A version like 4.2.1 follows semantic versioning: 4 is the major version (breaking changes), 2 is the minor version (new features, backwards compatible), and 1 is the patch version (bug fixes). The caret symbol ^4.2.1 means npm can install any version from 4.2.1 up to but not including 5.0.0. This is the default when you npm install a package. It assumes minor and patch updates are safe. The tilde symbol ~4.2.1 is more conservative — it allows patches only, so 4.2.1 up to but not including 4.3.0. An exact version 4.2.1 with no symbol means exactly that version and nothing else. Run: npm install lodash@4.17.21 to install a specific version. Run: npm install lodash@^4.0.0 to install the latest 4.x version. Ask Claude Code: Review my package.json version ranges and flag any that are too loose or too strict. Explain the risk of each. Too-loose ranges (like * or >=1.0.0) can pull in breaking changes. Too-strict ranges (exact versions everywhere) mean you miss security patches. The sweet spot is caret ranges for most packages and exact versions for packages that have caused breaking changes in minor releases. Common error: if you see "Could not resolve dependency" during npm install, it usually means two packages require incompatible versions of a shared dependency. Ask Claude Code: I am getting a dependency conflict when running npm install. Help me resolve it. Claude Code will read the error, identify the conflicting packages, and suggest a resolution — often using npm install --legacy-peer-deps or updating one of the conflicting packages.
Lock files and reproducible builds
The package-lock.json file is one of the most misunderstood files in Node.js projects. Many developers delete it or add it to .gitignore — both are mistakes. When you run npm install, npm resolves your version ranges to specific versions. If your package.json says ^4.2.1, npm might install 4.5.3 today. But next month, 4.6.0 might be released, and a fresh npm install would get 4.6.0 instead. The lock file prevents this drift. It records the exact version of every package (and every sub-dependency) that was installed. When another developer clones your project and runs npm install, they get exactly the same versions you used. This is called a reproducible build — the same code, the same dependencies, the same behaviour on every machine. Always commit package-lock.json to git. Run: git add package-lock.json. If you see merge conflicts in package-lock.json after a git merge, do not try to resolve them manually. Instead, accept either version and run: npm install. This regenerates the lock file correctly. Ask Claude Code: Explain the difference between npm install and npm ci. The npm ci command (clean install) is stricter: it deletes node_modules entirely, installs exactly what the lock file specifies (ignoring package.json ranges), and fails if the lock file is out of sync with package.json. Use npm ci in CI/CD pipelines and production deployments for maximum reliability. Use npm install during development when you want to resolve and update ranges. Common error: "npm WARN old lockfile" means your lock file was generated by an older npm version. Run npm install to regenerate it with your current npm version.
npm scripts and automation
The scripts section of package.json is a powerful automation tool that most beginners overlook. Instead of remembering long commands, you define them once and run them with npm run. Open package.json and look at the scripts section. By default, you might see a test script. Add more: in the scripts object, set start to node src/index.js, set dev to node --watch src/index.js, set build to tsc, set lint to eslint src/, and set format to prettier --write src/. Now run: npm run dev. This executes node --watch src/index.js. The npm run prefix is required for custom scripts, but some scripts have shortcuts: npm start, npm test, and npm run build can be run without the run keyword for start and test. Ask Claude Code: Look at my project and suggest useful npm scripts for development, testing, building, and deployment. Scripts can be chained. Set prebuild to npm run lint — npm automatically runs prebuild before build. Set postbuild to echo Build complete — npm runs it after build. This pre/post convention works for any script name. For more complex automation, scripts can call other scripts: set validate to npm run lint && npm run test && npm run build. The double ampersand means each command must succeed before the next one runs. Common error: if a script fails with "command not found," the tool is probably a dev dependency that is not installed. Run npm install first. Another common issue: on Windows, some Unix commands in scripts (like rm -rf) do not work. Use the cross-env and rimraf packages for cross-platform compatibility. Ask Claude Code: Make my npm scripts work on both Windows and Mac.
Organising a real project with dependencies
Now put it all together by setting up a real project with a proper dependency structure. Create a new folder and run: npm init -y. Ask Claude Code: Set up a Node.js project with TypeScript, ESLint, Prettier, and Jest. Configure package.json with appropriate scripts for dev, build, test, lint, and format. Claude Code will install the right packages as dependencies or dev dependencies, create configuration files for each tool, and set up scripts that work together. Your final package.json should have dependencies like express or fastify for your runtime code and devDependencies like typescript, eslint, prettier, jest, and their type definitions. The scripts section should include: dev (run in development with hot reload), build (compile TypeScript to JavaScript), start (run the compiled code), test (run Jest), lint (check code quality), format (auto-format code), and validate (run all checks before committing). Ask Claude Code: Create a .npmrc file with sensible defaults for this project. A .npmrc file configures npm behaviour per project. Useful settings include save-exact=true (install exact versions instead of ranges), engine-strict=true (enforce Node.js version requirements), and auto-install-peers=true (automatically install peer dependencies). Finally, verify everything works. Run: npm run validate. If all scripts pass, your project is properly configured. Run: npm ls --depth=0 to see your direct dependencies. Run: npm ls to see the full dependency tree. Ask Claude Code: Audit my project setup and tell me if I am missing anything important. This systematic approach to project setup prevents the "it works on my machine" problem and gives every team member a consistent development experience.
Environment Setup
This guide is hands-on and practical. The full curriculum covers the conceptual foundations in depth with structured lessons and quizzes.
Go to lesson