15 questions found
What is the purpose of a package-lock.json file in a Node.js project, and why should it be committed to version control?
Beginner
package-lock.json records the exact resolved version of every installed dependency (including nested transitive dependencies), ensuring that running 'npm install' produces byte-for-byte identical dependency versions across every machine and CI run -- without it, package.json's version ranges (like '^1.2.0') could resolve to different actual versions over time as new compatible releases are published, potentially introducing subtle, hard-to-reproduce bugs between different installs.
// package.json allows a range of versions
"dependencies": { "express": "^4.18.0" }
// package-lock.json pins the exact resolved version and its full dependency tree
"express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz"
}
Real-world example
A team debugging a bug that only reproduced in production eventually traced it to a subtle behavior difference introduced by a transitive dependency's patch version, which had drifted between a developer's local install and the CI/production install because package-lock.json hadn't been committed to version control.
Common follow-ups: What's the difference between 'npm install' and 'npm ci' regarding how they use (or don't use) package-lock.json?;Why is it considered bad practice to manually edit package-lock.json by hand?
npm & Packages;CI/CD
Publishing & Deployment
What is a monorepo, and what specific tooling (like npm workspaces or Turborepo) helps manage multiple related Node.js packages within a single repository?
Intermediate
A monorepo houses multiple related, independently versionable packages (a shared library, a backend API, a frontend app) within a single Git repository, rather than splitting them across separate repositories -- npm workspaces (built into npm) let these packages reference each other locally without needing to be published to a registry first, while tools like Turborepo or Nx add build-caching and task-orchestration on top, only rebuilding and retesting packages actually affected by a given change.
// package.json at the repo root
{
"workspaces": ["packages/*"]
}
// packages/api/package.json can depend on a sibling package directly
"dependencies": { "@myorg/shared-utils": "workspace:*" }
Real-world example
A company splits their shared validation logic, backend API, and admin dashboard into separate npm workspace packages within one monorepo, letting the API import the shared validation package directly during local development without needing to publish it to a private registry first.
Common follow-ups: What's the tradeoff between a monorepo and separate repositories for related but independently deployable services?;How does a tool like Turborepo's build caching actually determine which packages need to be rebuilt after a given change?
npm & Packages;CI/CD
Publishing & Deployment
How would you use Git hooks (like a pre-commit hook via Husky) to enforce code quality checks before code is committed in a Node.js project?
Advanced
Git hooks are scripts that run automatically at specific points in the Git workflow (before a commit, before a push) -- Husky is a popular tool that makes configuring these hooks straightforward within a Node.js project's package.json, commonly used to run a linter, formatter, or a fast subset of tests against only the staged files before allowing a commit to proceed, catching basic issues locally before they ever reach a shared branch or CI.
// package.json
"husky": { "hooks": { "pre-commit": "lint-staged" } },
"lint-staged": { "*.js": ["eslint --fix", "prettier --write"] }
// This automatically lints and formats only the files being committed,
// before the commit is actually allowed to complete
Real-world example
A team configures Husky and lint-staged to automatically run ESLint and Prettier on only the files being committed, catching style violations and simple bugs locally before a developer even pushes, rather than discovering them later in a CI pipeline or code review.
Common follow-ups: Why does lint-staged specifically run checks only against staged files rather than the entire codebase on every commit?;What's the risk of a pre-commit hook that takes too long to run, and how do teams balance thoroughness against developer friction?
Testing with Jest
Mocha & the Node Test Runner;CI/CD
Publishing & Deployment
What is semantic versioning (semver), and how does it inform how a Node.js package's version number should change between releases?
Intermediate
Semantic versioning uses a MAJOR.MINOR.PATCH format, where a PATCH increment indicates a backward-compatible bug fix, a MINOR increment indicates new backward-compatible functionality, and a MAJOR increment indicates a breaking change -- consumers of a package rely on this convention (combined with caret/tilde ranges in package.json) to safely receive automatic bug-fix and feature updates without breaking their application, while being explicitly warned (and required to manually opt in) when a breaking major version change occurs.
// package.json version ranges leveraging semver conventions
"dependencies": {
"lodash": "^4.17.0", // accepts any 4.x.x update, but not 5.0.0
"express": "~4.18.0" // accepts only patch updates within 4.18.x
}
Real-world example
A library maintainer accidentally introduces a breaking API change in what was tagged as a minor version release, causing widespread downstream failures for consumers who had reasonably trusted the semver convention that a minor bump wouldn't break their code; the fix involves yanking the release and re-publishing the breaking change as a proper major version instead.
Common follow-ups: What's the practical consequence for downstream consumers when a maintainer violates semver conventions like this?;How do automated tools like semantic-release determine the appropriate version bump automatically based on commit messages?
npm & Packages;CI/CD
Publishing & Deployment
How would you structure conventional commit messages in a Node.js project, and how do they enable automated changelog generation?
Advanced
Conventional Commits is a specification for structuring commit messages with a type prefix (feat, fix, docs, chore, BREAKING CHANGE, and others) followed by a concise description -- tooling like semantic-release or standard-version can parse this consistent format across a project's commit history to automatically determine the appropriate semver version bump and generate a categorized changelog, removing the need for manual, error-prone changelog maintenance.
feat: add support for refresh token rotation
fix: correct off-by-one error in pagination
docs: update README with new API examples
feat!: remove deprecated v1 authentication endpoints
BREAKING CHANGE: the /v1/auth endpoints have been removed; use /v2/auth instead
Real-world example
A team adopts conventional commits and configures semantic-release in their CI pipeline, so merging a commit prefixed with 'fix:' automatically triggers a patch release with an auto-generated changelog entry, entirely removing a previously manual and often-forgotten release process step.
Common follow-ups: How does semantic-release determine whether to publish a patch, minor, or major version based on the commit history since the last release?;What happens if a team's commits don't consistently follow the convention -- does the automation simply fail silently?
npm & Packages;CI/CD
Publishing & Deployment
What is the purpose of a .gitignore file in a Node.js project, and what should always be included in it?
Beginner
.gitignore tells Git which files and directories to never track or commit -- for a Node.js project this critically includes node_modules (which should always be reinstalled from package-lock.json rather than committed, given its size and platform-specific native binaries), .env files containing secrets, build output directories, and log files, keeping the repository focused on source code and configuration rather than generated or sensitive artifacts.
# .gitignore
node_modules/
.env
.env.local
dist/
*.log
coverage/
Real-world example
A new contributor accidentally committed their local .env file containing real API keys to a public repository before the team had a proper .gitignore in place, requiring the keys to be rotated immediately and prompting the team to add a comprehensive .gitignore to prevent recurrence.
Common follow-ups: What's the correct process for removing a sensitive file from Git history after it's already been committed and pushed?;Why shouldn't node_modules ever be committed, even though it would technically make cloning and running the project 'simpler'?
Security;npm & Packages
How would you set up branch protection rules and required status checks for a Node.js repository to prevent broken code from being merged?
Intermediate
Branch protection rules (configured on the Git hosting platform, like GitHub) can require that a pull request pass specific CI checks (tests, linting, build) before it's allowed to merge, require at least one approving code review, and prevent direct pushes to the main branch entirely -- combined with a CI pipeline that runs these checks automatically on every pull request, this ensures broken or unreviewed code can't reach the main branch regardless of who's making the change.
# GitHub branch protection settings (conceptual, configured via the UI or API)
required_status_checks: ['test', 'lint', 'build']
required_approving_review_count: 1
enforce_admins: true
restrict_pushes: true # no direct pushes to main, only via pull request
Real-world example
A team enables branch protection on their main branch requiring the CI test suite and linter to pass along with at least one code review approval, preventing an incident where a developer had previously pushed directly to main with a change that broke production.
Common follow-ups: Should branch protection rules apply equally to repository administrators, or is an exception sometimes justified?;How do you handle an urgent hotfix that needs to bypass the normal review process during an active incident?
CI/CD
Publishing & Deployment;engineering:code-review
How would you use Git bisect to find the specific commit that introduced a regression in a Node.js application's behavior?
Advanced
git bisect performs an efficient binary search through a range of commits, at each step asking you (or an automated test script) to mark the current commit as 'good' or 'bad', narrowing down the search space by half each time until it identifies the exact commit that introduced the regression -- combined with an automated test script that can programmatically determine good/bad status (via 'git bisect run'), this can pinpoint a regression's origin across potentially hundreds of commits in just a handful of steps.
git bisect start
git bisect bad HEAD # current commit exhibits the bug
git bisect good v1.2.0 # this earlier tagged commit was known good
# Automated: runs a test script at each step to determine good/bad automatically
git bisect run npm test
Real-world example
A team noticing a subtle performance regression that appeared sometime in the last month uses git bisect run with an automated benchmark script, pinpointing the exact commit that introduced a slow database query change out of over 200 commits in the affected range within just eight bisection steps.
Common follow-ups: What makes a good automated test script for 'git bisect run', and what specific exit codes does it need to return?;What happens if a commit within the bisected range simply doesn't build or can't be tested at all?
Debugging & Diagnostics;Testing with Jest
Mocha & the Node Test Runner
What is the difference between npm, Yarn, and pnpm as Node.js package managers, and what specific advantage does pnpm offer regarding disk space?
Intermediate
All three install and manage a project's dependencies from the npm registry, offering broadly similar core functionality, but with different underlying implementations -- pnpm's key differentiator is its content-addressable storage approach, where every version of every package is stored once globally on disk and hard-linked into each project's node_modules, dramatically reducing disk usage and installation time when many projects on the same machine share overlapping dependencies, compared to npm and Yarn (in its default mode) which each duplicate a full copy per project.
# All three achieve a similar end result with different mechanics
npm install
yarn install
pnpm install # uses a global content-addressable store, hard-linked per project
Real-world example
A developer working across a dozen different Node.js projects notices their disk usage drop significantly after switching from npm to pnpm, since previously near-identical dependency trees across projects were each fully duplicated, while pnpm's hard-linking approach stores each unique package version only once.
Common follow-ups: What's the tradeoff or compatibility consideration when switching an existing project from npm to pnpm, given pnpm's stricter node_modules structure?;How does pnpm's approach affect a project's build reproducibility compared to npm or Yarn?
npm & Packages;Performance Optimization & Profiling
How would you configure a Node.js project's package.json to support both being used as a library (imported by others) and having its own development scripts, distinguishing dependencies from devDependencies correctly?
Advanced
dependencies lists packages required for the package to actually function at runtime when installed by a consumer (these get installed transitively when someone depends on your package), while devDependencies lists packages needed only for developing, testing, or building the package itself (a test framework, a linter, a bundler) that consumers of the published package never need installed -- correctly categorizing these matters significantly for keeping a published package's actual install footprint minimal for its consumers.
{
"dependencies": { "express": "^4.18.0" }, // needed at runtime by consumers
"devDependencies": {
"jest": "^29.0.0", // only needed for running this project's own tests
"eslint": "^8.0.0" // only needed for linting during development
}
}
Real-world example
A published npm package accidentally listed its testing framework under 'dependencies' instead of 'devDependencies', meaning every single consumer installing the package also unnecessarily downloaded the entire test framework and its own dependency tree; moving it to devDependencies immediately reduced the package's install footprint for everyone depending on it.
Common follow-ups: What's the purpose of a third category, peerDependencies, and when should a package use it instead of a regular dependency?;How does 'npm install --production' or 'npm ci --omit=dev' leverage this distinction in a deployment context?
npm & Packages;CI/CD
Publishing & Deployment