You close Module 4 with a complete web platform —server, router, static files, orders, HTTP client— and without a single external dependency. That was a deliberate choice and it has paid off enormously: you now know what sits underneath. But it also had a price you paid by hand, line by line: compiling route patterns, parsing bodies, maintaining a MIME type table, computing ETags.
That price is not always worth paying. A real project runs into hundreds of problems other people have already solved, better than you would solve them in an afternoon, and battle-tested by millions of installs. The craft lies in knowing when to lean on that work and when not to, and for that you need to understand the machinery: npm.
Until now Escena Viva has lived without a package.json. You run it with node src/... and it works. That changes in this lesson: we give the project a formal identity, a manifest that declares what it is, which version of Node it runs on and —starting with the next lesson— what it depends on.
Contents
- What npm is: two things sharing one name
- The ecosystem and its alternatives: yarn, pnpm, bun
npm init: the Escena Viva manifest- The fields of
package.json, one by one - The complete
package.jsonfor Escena Viva node_modules: what it is and why it never gets committednpm installwith no argumentsnpx: running without installing- Where configuration lives:
npm configand.npmrc
- What npm is: two things sharing one name
When someone says "npm" they may be talking about two different things, and mixing them up causes constant misunderstandings.
| The registry | The tool | |
|---|---|---|
| What it is | A public package server at https://registry.npmjs.org |
The npm command-line program |
| What it does | Stores and serves packages published by anyone | Downloads, installs, versions, publishes and runs scripts |
| Can be swapped for | Private registries (Verdaccio, Artifactory, GitHub Packages) | yarn, pnpm, bun |
| Installation | None, it is a service | Comes bundled with Node.js |
The second row is the important one: the registry and the tool are independently interchangeable. You can use pnpm against the public registry, or npm against your company's internal registry. They are not married.
Check that you already have the tool, because the Node installer brought it along:
If you use nvm or fnm, every Node version ships its own npm. When you switch Node versions with nvm use, you switch npm too without noticing. That is normal and it is correct.
Why it is the largest ecosystem in the world
The public registry holds more than three million packages, more than any other language ecosystem. The reasons are structural:
- JavaScript runs everywhere: browser, server, mobile, desktop, serverless functions. A single registry serves all those worlds.
- Publishing is trivial: a
package.jsonwith three fields andnpm publish. There is no review board. - JavaScript's standard library was historically small. Where other languages ship date handling or collection utilities out of the box, JavaScript did not, and the registry filled the gap. Hence its reputation for fragmentation and single-function packages. Today Node's core has grown a great deal —you have seen it across four modules— and many classic dependencies are no longer needed.
The flip side of "publishing is trivial" is that anyone can publish anything. That turns choosing dependencies into an engineering decision rather than a formality: we cover it in 05-02 with a checklist, and in 05-06 from the security angle.
- The ecosystem and its alternatives: yarn, pnpm, bun
The npm tool is not the only one that knows how to read a package.json and install dependencies.
| Tool | Lockfile | On-disk layout | Strength | Weakness |
|---|---|---|---|---|
| npm | package-lock.json |
Flattened tree in node_modules |
Ships with Node; zero setup; universal | Neither the fastest nor the most compact |
| yarn (modern) | yarn.lock |
Flattened or Plug'n'Play with no node_modules |
Mature workspaces, PnP | PnP breaks tools that assume node_modules |
| pnpm | pnpm-lock.yaml |
Global store + hard links | Massive disk savings; strict about undeclared dependencies | Links confuse some older tools |
| bun | bun.lock |
Flattened, custom installer | Extreme speed; it is also a runtime | Young ecosystem; compatibility is not total |
The pnpm idea deserves a paragraph because it is genuinely different. Instead of copying every package into each project's node_modules, it keeps a single copy of each version in a global store on your machine (~/.local/share/pnpm/store) and creates links inside the project. If you have ten projects using [email protected], there is one express on disk, not ten: on a laptop with twenty repositories the savings are measured in gigabytes. On top of that pnpm is strict: if your code calls require('anything') without having declared it, it fails. With npm it often works by accident, because flattening leaves that transitive dependency visible at the root (05-02).
Why this course uses npm, even knowing there are faster options: it ships with Node (zero setup, zero divergence between machines); it is the common denominator that all the documentation and every continuous integration system understands; and the concepts are the same —package.json, semver, lock, scripts, registry— so moving to pnpm is five minutes with an equivalence table.
A practical rule for living together: one tool per project. Mixing npm install and yarn add in the same repository produces two contradictory lockfiles and hours of pointless debugging.
npm init: the Escena Viva manifest
npm init: the Escena Viva manifestThe package.json is a JSON file at the root of the project that plays four roles at once: identity (what it is called, what version it is, who maintains it), execution contract (which Node version it needs, whether it is CommonJS or ESM, what its entry point is), dependency declaration (05-02 and 05-03) and task interface through the scripts field (05-04).
Creating it takes a single command. From the project root:
npm init opens an interactive questionnaire with default values in parentheses; pressing Enter accepts the proposed one:
package name: (escena-viva) version: (1.0.0) 0.1.0 description: Ticket sales platform for cultural events entry point: (index.js) src/server/server.js test command: git repository: keywords: tickets,events,theater author: Escena Viva Team <[email protected]> license: (ISC) MIT
Two details: the default name is the folder's name —if yours is called Escena Viva, with capitals and a space, npm init will complain because of the rules in the next section— and we entered 0.1.0, not 1.0.0, because npm's default value means "stable API, I promise compatibility" and that is a bad promise for a project that is just starting. In 05-03 we will make that jump deliberately.
If you want to skip the questionnaire and accept every default:
It generates a minimal package.json in an instant. It is perfect for a throwaway lab, and you can always edit it afterwards: package.json is an ordinary text file, there is nothing magical about it. In fact, editing it by hand is the norm once the project exists.
- The fields of
package.json, one by one
package.json, one by onename
The package identifier. Its rules are strict because it ends up as part of a URL: 214 characters maximum, lowercase only (EscenaViva is invalid), no spaces —hyphens are used instead: escena-viva—, it cannot start with . or _, and it must be URL-safe: no accents, no ñ, no ·.
There is a variant with a scope, a prefix with an at sign: @escena-viva/format. The scope is a namespace reserved for a person or an organization, and it has three advantages: it avoids collisions (@escena-viva/format is yours even if format is taken), it groups packages belonging to one team, and it is private by default when publishing, which prevents accidental leaks (05-05). You will see them constantly: @babel/core, @types/node, @eslint/js.
version
The current version of the package in semver format: MAJOR.MINOR.PATCH. It is mandatory in order to publish and it is the backbone of lesson 05-03. For now, just remember that 0.1.0 says "this is still moving".
description and keywords
Free text and a list of keywords. They are search metadata: they feed the search engine on npmjs.com. In a private package they serve no functional purpose, but description is still useful as a reminder of what this repository is.
main
The file that gets loaded when someone calls require('escena-viva'). It is the entry point of the library, not necessarily of the executable. If it is omitted, Node looks for index.js at the root.
In Escena Viva we point it at src/server/server.js, which exports createServer without starting anything thanks to the require.main === module guard from Module 4. That is exactly what you want from an entry point: importing must have no side effects.
The modern field that replaces
mainwith far more control is calledexports, and we will meet it in 05-05 when we build a publishable package.
type
It decides how Node interprets this project's .js files:
| Value | .js files are read as |
require |
import |
|---|---|---|---|
"commonjs" (default) |
CommonJS | Yes | Dynamic only, or in .mjs |
"module" |
ES Modules | No (use .cjs) |
Yes |
In Module 2 we mentioned this line without developing it; now it has a place to live. Escena Viva declares "type": "commonjs" explicitly. It is the default value, so technically it is redundant, but writing it removes all ambiguity for whoever reads the project and for the tooling.
engines
Declares which Node versions the project supports:
This must be consistent with the .nvmrc you created in Module 1, but they are two files with different audiences: .nvmrc is read by nvm, fnm, Volta and CI, and it changes your active Node version; engines is read by npm and by deployment platforms, and it warns or fails if the version does not fit. By default it only warns; to turn it into a real error you have to enable it in the .npmrc from section 9.
private
A safety switch: with private: true, npm publish refuses to run. Nothing more, and nothing less.
Escena Viva is an application, not a library: it must never end up in the public registry. Putting private: true in every application is a cheap habit that has prevented many leaks of proprietary code caused by an npm publish typed in the wrong folder. You only remove it when you genuinely want to publish, which is what we will do in 05-05 with the @escena-viva/format package.
license
The SPDX identifier of the license: MIT, Apache-2.0, GPL-3.0-only, ISC. For closed source, the conventional value is:
It is not a decorative field: in 05-06 we will see how the licenses of the whole dependency tree are audited, and that analysis leans on precisely this field.
author and repository
"author": "Escena Viva Team <[email protected]>",
"repository": {
"type": "git",
"url": "git+https://github.com/escena-viva/escena-viva.git"
}repository is more useful than it looks: npm repo <package> opens that URL, and audit tools use it to locate a dependency's source code. When you are investigating whether a package can be trusted, the absence of repository is already a signal.
- The complete
package.json for Escena Viva
package.json for Escena VivaThis is the manifest we start the module with. It still has no dependencies —they arrive in 05-02— and no useful scripts —they arrive in 05-04— and its version is 0.1.0.
{
"name": "escena-viva",
"version": "0.1.0",
"private": true,
"description": "Ticket sales platform for cultural events",
"keywords": ["tickets", "events", "theater", "capacity"],
"license": "MIT",
"author": "Escena Viva Team <[email protected]>",
"type": "commonjs",
"main": "src/server/server.js",
"engines": {
"node": ">=24.5.0 <25"
},
"repository": {
"type": "git",
"url": "git+https://github.com/escena-viva/escena-viva.git"
},
"scripts": {
"start": "node src/server/server.js"
}
}Go over what that file tells a first-time reader without opening anything else: it is a ticket sales platform, at an early version, private (not published), CommonJS, needs Node 24, starts with npm start and its code lives in a known git repository. Eight lines of metadata that save an entire conversation.
One warning: package.json is strict JSON, with no comments, no trailing commas and double quotes throughout. If npm fails with Unexpected token, it is almost always an extra comma before a }. Check that npm understands it:
npm pkg get and npm pkg set read and write fields without opening an editor, preserving the formatting. They are the right way to touch the manifest from a script:
node_modules: what it is and why it never gets committed
node_modules: what it is and why it never gets committednode_modules is the folder where npm drops the code of the downloaded dependencies. It does not exist yet in Escena Viva because there are no dependencies, but it is worth understanding before creating it.
When you studied require resolution in Module 2, you saw that Node, faced with require('something') without a ./, walks up the directory tree looking for node_modules/something at every level. npm is simply the one who fills those folders: it writes files where Node already knew to look, with no extra magic. And it is a huge folder: a modest Express project runs to around 40,000 files.
It never goes into version control. The reasons, in order of importance:
| Reason | Explanation |
|---|---|
| It is reproducible | package.json + package-lock.json are enough to rebuild it identically. Storing the result alongside the source is redundant. |
| Size and noise | Tens of thousands of files turn every git status and every code review into something unmanageable. |
| Binary, platform-specific content | Some packages compile native binaries for your operating system and architecture. What works on your macOS may not start on the server's Linux. |
| Unresolvable conflicts | A merge conflict inside node_modules is not resolved; it is deleted and reinstalled. |
That is why the .gitignore you created in Module 1 already started with that line:
The first line is the critical one. The second one will be too from the next lesson onwards, when dotenv comes into play. And watch out for the asymmetry, which is the most common source of confusion in this module:
node_modules/→ ignored (it is the result).package.jsonandpackage-lock.json→ always committed (they are the source).
npm install with no arguments
npm install with no argumentsRun without a package name, npm install (or its alias npm i) means: "put this project in a state where it can run". It is the first thing anyone types after cloning a repository.
Its exact steps, in order: it reads package.json and gathers dependencies and devDependencies; it reads package-lock.json if it exists, to learn the already-resolved tree; it compares against what is installed and works out what is missing or superfluous; it downloads what is missing from the registry, using the local cache when it can; it verifies the integrity of each package against its hash (integrity); it extracts into node_modules, flattening the tree; it links the executables into node_modules/.bin (key for 05-04); it runs the install scripts of packages that have them (postinstall — take note, 05-06); and finally it updates package-lock.json if anything new had to be resolved.
That last step is the essential difference from npm ci, and we will devote a whole table to it in 05-03: npm install may modify the lock; npm ci never touches it.
In Escena Viva, today, the result is anticlimactic and instructive:
"1 package" is the project itself. Zero dependencies, zero vulnerabilities: the safest possible state, and the baseline against which we will compare everything we add.
npx: running without installing
npx: running without installingThere are two ways to use a command-line tool published on npm.
The old way, a global install:
The recommended way, npx:
npx looks in this order: if the executable exists in the project's node_modules/.bin, it uses it; if it is in the npx cache, it uses it; and if not, it downloads it into a temporary cache, runs it and does not install it in the project.
Global install (-g) |
npx |
|
|---|---|---|
| Where it lands | On your system, forever | In the cache; it does not pollute the project |
| Version | The one you installed months ago | The one declared by the project, or the latest |
| Team consistency | Everyone has their own | Everyone uses the same one |
| Ideal use | Daily-use tools unrelated to projects | Almost everything else |
The real problem with -g is not disk space, it is version drift: you scaffold a project with the global version you installed in March and your teammate with the one they installed yesterday, and the results differ without anyone understanding why.
You can pin the version, which is the advisable thing to do in documentation and in CI:
And the point that closes the circle: if the project declares a tool in devDependencies, npx eslint uses that local copy, downloading nothing. That is why in 05-04 we will be able to write "lint": "eslint src" in the scripts without installing ESLint globally.
- Where configuration lives:
npm config and .npmrc
npm config and .npmrcnpm is configured in layers. To see the effective result:
; "user" config from /home/user/.npmrc init-author-name = "Escena Viva Team" ; node bin location = /home/user/.nvm/versions/node/v24.5.0/bin/node ; cwd = /home/user/escena-viva ; npm version = 11.4.2
With npm config list -l you will also see every default value. The layers are applied in this order, from highest to lowest priority:
| Layer | Location | Typical use |
|---|---|---|
| Command line | --registry=... |
One specific run |
| Environment variables | NPM_CONFIG_REGISTRY |
CI and containers |
Project .npmrc |
./.npmrc |
Committed to the repo: team policy |
User .npmrc |
~/.npmrc |
Not committed: your tokens |
Global .npmrc |
Next to the npm installation | Rarely touched |
| Default values | Built in | — |
Escena Viva's project .npmrc, which does go into the repository:
# Fail the install if the Node version does not satisfy "engines"
engine-strict=true
# When using "npm install <package>", save an exact range instead of "^"
# (we will justify this in 05-03; for now it is just noted)
save-exact=false
# Default registry, spelled out so nobody has to wonder
registry=https://registry.npmjs.org/engine-strict=true turns the engines warning into an error: if someone tries to install the project with Node 22, npm digs in its heels. Combined with .nvmrc, the Node version stops being a hallway convention and becomes a verified rule.
And the most important warning in this section: the user .npmrc (~/.npmrc) contains your authentication tokens, in lines of the form //registry.npmjs.org/:_authToken=npm_xxxxxxxx. That file is never uploaded anywhere: a token leaked in a public repository lets anyone publish packages in your name (05-05 and 05-06).
Finally, the default registry is https://registry.npmjs.org/. Changing it has two legitimate use cases: an internal company mirror and a private registry for proprietary packages, usually combined with scopes:
# Only @escena-viva/* packages go to the internal registry
@escena-viva:registry=https://internal.registry.test/Common Mistakes and Tips
- Running
npm initin the wrong folder. Check withpwdfirst. If you get it wrong, delete thepackage.jsonyou created: an orphan manifest in your home folder makes npm treat that whole folder as a project. - An invalid
package.json. No comments, no trailing commas, double quotes. Validate it quickly withnode -e "require('./package.json')": if it prints no error, the JSON is correct. - Confusing
mainwith "the file that starts the application".mainis what you get when you import the package; what starts the application isscripts.start. Their coinciding here is a convenience, not a rule. - Committing
node_modulesbecause you forgot the.gitignore. If you already committed it:git rm -r --cached node_modules, add the line and commit. The history will still be heavy, but the damage stops growing. - Installing tools with
-gout of habit. Every-gis a version that exists only on your machine. Usenpx, or declare it indevDependencies. - Tip:
npm init -yfollowed bynpm pkg set. Faster than the questionnaire and automatable in a project template. And usenpm pkg getinstead of reading the file withgrep: it returns valid JSON and does not break when the formatting changes.
Exercises
Exercise 1. Create the Escena Viva manifest.
From the project root, run npm init and answer the questionnaire to produce exactly the package.json from section 5: name escena-viva, version 0.1.0, MIT license, entry point src/server/server.js. Then add private, type and engines by hand. Verify with npm pkg get and check that npm start boots the Module 4 server.
Exercise 2. Check that engine-strict works.
Create the project .npmrc with engine-strict=true. Temporarily change engines.node to ">=99.0.0" and run npm install. Observe the exact error. Then remove engine-strict and repeat: what changes? Leave the file as it was.
Exercise 3. npx versus -g.
Without installing anything globally, run npx [email protected] "Capacity 3000" and note what npx prints the first time and what it prints the second time. Then find where the cache lives with npm config get cache and explain in two sentences why npx gives more reproducible results across a team than a global install.
Solutions
Solution 1. After the questionnaire, the generated file does not include private, type or engines; you add them by hand or with npm pkg:
npm pkg set private=true --json
npm pkg set type="commonjs"
npm pkg set engines.node=">=24.5.0 <25"
npm pkg set scripts.start="node src/server/server.js"The --json on the first line is essential: without it, private would be stored as the string "true" and not as the boolean true, and npm would not read it as a switch. It is the most frequent mistake with npm pkg set.
After that, npm start must boot the server exactly as node src/server/server.js does, because that is literally what it runs.
Solution 2. With engine-strict=true and an impossible engines.node:
npm error code EBADENGINE npm error engine Unsupported engine npm error engine Not compatible with your version of node/npm: [email protected] npm error notsup Required: {"node":">=99.0.0"} npm error notsup Actual: {"npm":"11.4.2","node":"v24.5.0"}
The install stops with a non-zero exit code. Without engine-strict, the same case produces only an npm warn EBADENGINE and the install continues. The difference matters above all in the Module 11 CI: a warning goes unnoticed in a log of thousands of lines; a failure stops the deployment.
Solution 3. The first run downloads the package and announces it:
Need to install the following packages: [email protected] Ok to proceed? (y)
The second finds it in the cache and runs instantly, without asking. The cache lives wherever npm config get cache says (~/.npm/_npx for temporary executables).
Reproducibility comes from two things: npx package@version pins the version in the command itself, so the same command produces the same result on any machine; and when the project declares the tool in devDependencies, npx uses the project's local copy, which is pinned in package-lock.json and identical for the whole team. A global install offers neither guarantee: it depends on when each person installed it.
Conclusion
Escena Viva now has a manifest. You have seen that npm is two things: a public registry with more than three million packages and a command-line tool bundled with Node, and that either can be swapped out independently —the course uses npm for its ubiquity, knowing that pnpm saves disk space with its global store and links, and that yarn and bun play the same game by different rules.
The package.json you created fulfills the four roles from section 3: identity (name, version, description, author, repository), execution contract (type: commonjs, main, engines consistent with the .nvmrc from Module 1), and the two gaps we will fill shortly: dependencies and scripts. And it carries private: true, the cheap switch that stops an application from being published by accident.
You also know that node_modules is just the place where npm drops what Node already knew how to look for since Module 2, that it never goes into the repository because it is reproducible from package.json and the lock, and that npm install with no arguments runs nine very specific steps —of which the last one, touching the lock, is precisely the one npm ci does not do. That npx runs tools without installing them and avoids the version drift caused by -g. And that configuration lives in layers, with a project .npmrc that gets committed (engine-strict=true) and a user one that holds your tokens and is never committed.
In the next lesson, Installing and Using Packages, we finally break the zero-dependency streak: we will install the first ones, tell dependencies apart from devDependencies and see why that distinction changes the size and the security of what you deploy, and —more important than any command— we will build a checklist for deciding whether a dependency deserves to enter the project. Because after writing a whole module without a single one, you already know the answer is not always yes.
Node.js Course: From Beginner to Advanced
Module 1: Introduction to Node.js
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
