# The NestJS mistakes language models actually make

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

Nest's structure means generated code lands in predictable places, which makes review fast. The bugs that survive are not type errors — they are configuration and authorisation, and both are invisible to the compiler.

## Security

### 1. Authentication without authorisation

The most common real vulnerability in generated back-end code, in any framework.

```ts
@Get(':id')
findOne(@Param('id') id: string) {
  return this.invoices.findOne(id);      // any logged-in user, any invoice
}
```

The guard confirmed *a* valid user. Nothing confirmed it is *their* invoice.

**Correct:** the ownership check goes in the service, beside the query.

```ts
async findOne(id: string, requester: AuthUser) {
  const inv = await this.repo.findById(id);
  if (!inv || inv.ownerId !== requester.id) throw new NotFoundException();
  return inv;
}
```

Return 404, not 403, for a resource the caller may not see — a 403 confirms the id is real.

**Catch it with:** one test per resource type. It is the highest value-per-line test in the codebase.

```ts
it('does not leak another user’s invoice', () =>
  request(app.getHttpServer())
    .get(`/invoices/${bobsInvoice.id}`)
    .set('Authorization', aliceToken)
    .expect(404));
```

### 2. Guards applied per-route instead of globally

```ts
@UseGuards(JwtAuthGuard)          // on each controller that needs it
```

This fails open: the endpoint someone forgets is public. Register the guard with `APP_GUARD` and opt out with `@Public()`.

```ts
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }]
```

**Catch it with:** `grep -rn "@UseGuards" src` — in a correctly configured app there should be very few.

### 3. `ValidationPipe` with no options

```ts
app.useGlobalPipes(new ValidationPipe());   // validates declared fields only
```

Undeclared properties pass straight through. Combined with the next item, that is privilege escalation.

**Correct:** `new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })`.

### 4. Mass assignment

```ts
await this.prisma.user.update({ where: { id }, data: { ...dto } });
```

If anything undeclared survived validation, it is now written to the database. A client sending `{ "role": "admin" }` gets exactly what it asked for.

**Correct:** pick fields explicitly, or rely on `whitelist: true` and still pick fields explicitly.

**Catch it with:** `grep -rn "data: { \.\.\." src`

### 5. Entities serialised with their secrets

```ts
return this.repo.findOne(id);     // returns passwordHash, refreshToken, ...
```

**Correct:** `@Exclude()` on the field plus `ClassSerializerInterceptor` registered globally.

**Catch it with:** `grep -rln "passwordHash\|refreshToken" src | xargs grep -L "@Exclude"`

## Validation

### 6. Nested DTOs that are never validated

```ts
@ValidateNested({ each: true })
lines!: OrderLineDto[];            // no @Type() — items are not checked
```

`class-transformer` needs `@Type(() => OrderLineDto)` to know what to instantiate. Without it, validation of the array items is silently skipped. Silent is the problem: nothing fails, nothing warns.

### 7. Query and param types

Query parameters are always strings. `@IsInt()` on a query DTO without `@Type(() => Number)` (or `enableImplicitConversion`) rejects every valid request.

### 8. Global pipes missing in tests

`useGlobalPipes` lives in `main.ts`, which the testing module never runs. Generated e2e tests therefore pass on payloads production would reject. Register the same pipes in the test setup.

## Architecture

### 9. Logic in controllers

```ts
@Post()
async create(@Body() dto: CreateOrderDto) {
  const user = await this.prisma.user.findUnique(...);   // in a controller
  if (!user) throw new NotFoundException();
  const total = dto.lines.reduce(...);
  return this.prisma.order.create(...);
}
```

Untestable without HTTP, unreusable, and it puts business rules somewhere nobody looks for them. **Catch it with:** `grep -rn "PrismaService\|Repository<" src --include="*.controller.ts"` — that should return nothing.

### 10. Request-scoped providers used casually

```ts
@Injectable({ scope: Scope.REQUEST })
```

Scope is contagious: everything that injects this becomes request-scoped too, all the way up to the controller. On a hot path that is a chain of object construction per request.

**Correct:** `AsyncLocalStorage`, or `nestjs-cls`, for request context.

### 11. `forwardRef` as a habit

One or two is a design signal. Several means a shared concept wants extracting into a third module.

### 12. Missing shutdown hooks

```ts
app.enableShutdownHooks();       // omitted in most generated bootstraps
```

Without it, `onModuleDestroy` never runs and database connections leak on every redeploy.

## Async

Generated Nest inherits every TypeScript and JavaScript async failure — floating promises are the big one, particularly in event handlers and background jobs. See [the TypeScript failure modes](https://learn-typescript.org/review/failure-modes/).

Nest-specific: an interceptor that does not return the observable, or an `async` guard whose promise is not awaited by the code path you think.

## The review, as five commands

```bash
grep -rn "PrismaService\|Repository<" src --include="*.controller.ts"
grep -rn "new ValidationPipe" src | grep -v whitelist
grep -rn "data: { \.\.\." src
grep -rn "@UseGuards" src
grep -rln "passwordHash\|refreshToken" src | xargs grep -L "@Exclude"
```

Put them in a `pnpm check:conventions` script and run it in CI. They cover the items on this page that actually cause incidents.

:::verdict The short version
Nest's conventions make everything else easy to review, so spend the attention on the two things the framework cannot check for you: does every resource endpoint verify ownership, and is `ValidationPipe` configured with `whitelist: true`.
:::

:::promo digitalocean
:::

## Common questions

### Why is the ownership check in the service and not a guard?

Because it depends on the record. A guard runs before the handler and would have to query the database itself, duplicating the work and drifting out of sync with the real query. Beside the query is where the check stays correct.

### Is `forbidNonWhitelisted` too strict?

For an internal API, no — a surprise field means a client bug and you want to hear about it. For a public API where clients may send extra fields harmlessly, `whitelist: true` alone (strip rather than reject) is the friendlier choice.

### Do I need all five grep checks in CI?

They take under a second and each one maps to a real incident class. The controller-database one and the ValidationPipe one are the two you should never skip.
