Review & Verify Updated 2026-09 9 min read View as Markdown

The NestJS mistakes language models actually make

TypeScript catches the type errors. What gets through is authorisation, validation configuration, and logic drifting out of services — and those are the ones that matter.

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.

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#

shell
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.

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.

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.

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.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.

Disclosure: some links on this page are affiliate links. If you buy something through one, we earn a commission at no extra cost to you. We only list tools we would tell a friend to use, and we say so when we have not used something ourselves. This is how the site stays free and ad-light.