# Dependency hygiene for NestJS projects

> Source: https://learn-nestjs.com/review/dependencies/
> Part of Learn NestJS, free to read.

Everything in [npm dependency hygiene](https://learn-javascript.org/review/dependencies/) 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.

```json
"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:

```text
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:

```bash
npm ls @nestjs/common @nestjs/core        # more than one version? that is your bug
pnpm why @nestjs/common
```

```bash
# audit alignment
npm ls --depth=0 2>/dev/null | grep '@nestjs/'
npx npm-check-updates '/@nestjs/.*/' -u   # upgrade the family together, never piecemeal
```

**Upgrade 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

```json
"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.

```bash
npm ls reflect-metadata      # must be one version, deduped
```

**`class-validator` and `class-transformer`** are pre-1.0 and their minor releases have historically been breaking. Pin them exactly rather than with a caret:

```json
"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.

```bash
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.

```dockerfile
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.

```bash
docker images | grep myapp        # is it 200MB or 1.2GB?
docker scout cves myapp:latest    # or trivy image myapp:latest
```

## Audit and update

```bash
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.

:::verdict The Nest-specific policy
1. `@nestjs/*` core packages share a major. Upgrade them together, in one commit.
2. `reflect-metadata` must be exactly one copy. Check when DI behaves oddly.
3. Pin `class-validator` and `class-transformer` exactly — pre-1.0, and validation is security.
4. Treat every peer dependency warning as a bug.
5. 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.
