Dependency hygiene for NestJS projects
npm's problems, plus a framework whose packages must all be on the same major version, plus a decorator ecosystem where a version mismatch produces a runtime error with no useful message.
Everything in npm dependency hygiene applies — install scripts, slopsquatting, lockfiles, transitive bloat — and that is the larger risk. This page is what is specific to Nest.
@nestjs/* packages must move together#
Nest is not one package; it is fifteen or twenty that share internals. Mixing majors produces failures that are genuinely hard to diagnose.
"dependencies": {
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
"@nestjs/config": "^4.0.0", // its own versioning — check the compat table
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.0",
"@nestjs/swagger": "^11.0.0",
"@nestjs/testing": "^11.0.0"
}The core packages (common, core, platform-*, testing, and most first-party modules) share a major version. Satellite packages like @nestjs/config and @nestjs/schedule have their own numbering, and each release documents which Nest major it supports.
The failure when they drift is characteristic and unhelpful:
Nest can't resolve dependencies of the FooService (?).
Please make sure that the argument Object at index [0] is available in the FooModule context.That message appears for a genuinely missing provider and for a version mismatch where two copies of @nestjs/common exist and the metadata keys do not match. If the provider is obviously present and you get this, check for duplicates:
npm ls @nestjs/common @nestjs/core # more than one version? that is your bug
pnpm why @nestjs/common# audit alignment
npm ls --depth=0 2>/dev/null | grep '@nestjs/'
npx npm-check-updates '/@nestjs/.*/' -u # upgrade the family together, never piecemealUpgrade the whole family in one commit. Bumping @nestjs/core alone is how you get the error above at 2am.
The decorator stack is tightly coupled#
"dependencies": {
"reflect-metadata": "^0.2.2",
"class-validator": "^0.14.0",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0"
}Four packages that are effectively part of the framework:
reflect-metadata must be imported exactly once, at the very top of main.ts, and there must be exactly one copy in the tree. Two copies means two metadata registries and dependency injection silently stops working for half your providers.
npm ls reflect-metadata # must be one version, dedupedclass-validator and class-transformer are pre-1.0 and their minor releases have historically been breaking. Pin them exactly rather than with a caret:
"class-validator": "0.14.2",
"class-transformer": "0.5.1"This is one of the few places where exact pinning is clearly worth the maintenance cost — a surprise change in validation behaviour is a security issue, not just a bug.
rxjs major versions matter because interceptors are built on it. A v7/v8 mismatch between your code and Nest's produces operator errors that look like your mistake.
Peer dependencies are not optional here#
Nest uses peer dependencies extensively, and npm 7+ installs them automatically — which hides a problem rather than solving it. A peer warning about @nestjs/common is telling you a module was built against a different major.
npm ls 2>&1 | grep -i 'peer dep\|invalid'Treat every one as a bug to fix rather than noise to scroll past. In this ecosystem they are usually correct.
Reduce the graph#
Nest applications accumulate dependencies quickly, partly because the documentation demonstrates a module for everything.
| Generated reaches for | Often unnecessary |
|---|---|
@nestjs/passport + a strategy for JWT only | @nestjs/jwt and a ~30-line guard |
bcrypt (native, needs a build toolchain) | argon2, or Node's crypto.scrypt |
moment | Intl, Temporal |
lodash | Object.groupBy, structuredClone, ?., ?? |
uuid | crypto.randomUUID() |
dotenv directly | @nestjs/config, which wraps it |
axios for one outbound call | global fetch (@nestjs/axios if you want DI) |
cache-manager + a store for one lookup | a Map with a TTL |
The Passport one is worth expanding. @nestjs/passport plus passport plus passport-jwt plus their types is four packages to validate a JWT — which @nestjs/jwt does in three lines inside a guard you already need to write. Passport earns its place when you have several OAuth providers; for JWT alone it is machinery.
bcrypt is worth flagging separately: it is a native module, so it needs a compiler in your build image, breaks on Node upgrades, and is a common cause of Docker build pain. argon2 is the better algorithm anyway, and @node-rs/argon2 avoids the native build.
Docker images get large#
A default Nest Dockerfile produces something close to a gigabyte. That is a deploy-time cost and a security surface.
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev # drop devDependencies
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node # do not run as root
CMD ["node", "dist/main.js"]Three things generated Dockerfiles miss: the multi-stage split (so build tools do not ship), npm prune --omit=dev or a production-only install, and USER node. The last one costs nothing and means a container escape starts from an unprivileged user.
docker images | grep myapp # is it 200MB or 1.2GB?
docker scout cves myapp:latest # or trivy image myapp:latestAudit and update#
npm audit --omit=dev # production tree only
npx depcheck # installed and never imported
npm ls --depth=0 | grep '@nestjs/'depcheck is worth running quarterly on a Nest project specifically, because modules get removed from app.module.ts far more often than their packages get removed from package.json. A @nestjs/graphql you stopped using two years ago is still in your tree, still in your image, still a surface.
The Nest-specific policy
@nestjs/*core packages share a major. Upgrade them together, in one commit.reflect-metadatamust be exactly one copy. Check when DI behaves oddly.- Pin
class-validatorandclass-transformerexactly — pre-1.0, and validation is security. - Treat every peer dependency warning as a bug.
- Multi-stage Docker build, prune dev dependencies,
USER node.
Common questions#
Why does Nest say it cannot resolve a provider that is clearly there?#
Two causes with the same message: the provider is genuinely not in scope (not in providers, or in another module that does not export it), or there are two copies of @nestjs/common in the tree so the metadata keys differ. Check npm ls @nestjs/common before you go hunting through modules.
Should I use Passport?#
For multiple OAuth providers, yes — that is what it is good at. For JWT validation alone it is four packages replacing a thirty-line guard, and the guard is easier to read and to debug than a strategy plus a module plus a serializer.
Is pinning class-validator exactly worth the maintenance?#
Yes. It is pre-1.0, its minors have been breaking historically, and a change in validation behaviour is a potential security regression rather than an inconvenience. Upgrade deliberately and read the changelog.
How do I keep the Docker image small?#
Multi-stage build, npm ci --omit=dev in the runtime stage or npm prune after building, an Alpine or distroless base, and a .dockerignore that excludes node_modules, .git and test files. That typically takes a default Nest image from around a gigabyte to a couple of hundred megabytes.
Get the NestJS agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for NestJS. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.