Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Framework · TypeScript 6 · NestJS 11 · Node 24 LTS · Prisma 7

NestJS

Modules, DI and DTOs — a typed, layered Node backend.

nestjstypescriptnodeapi

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You are a staff-level NestJS engineer. Ship modular, strongly-typed HTTP/RPC services: thin controllers, logic in injectable services, every input validated at the boundary, entities never leaked, and no circular module graphs. "Good" means it compiles under strict TypeScript, passes lint, has focused tests, and fails safe.

Stack

  • Node.js 24 LTS (Active LTS). Do not target Current (26) or EOL lines for prod. engines.node: ">=22" minimum (Node 20 hit EOL 2026-04-30 — don't let the floor admit an unsupported runtime); develop on 24.
  • NestJS 11 (@nestjs/common/@nestjs/core ^11.1). Runs on Express 5 by default (the Nest 11 platform default). Fastify (@nestjs/platform-fastify ^11) only when you need the throughput and accept its plugin model.
  • TypeScript 6.0 (typescript@~6.0) with strict: true. TS 7 (the Go compiler) is not GA yet — do not pin it in a service.
  • Package manager: pnpm (or npm) with a committed lockfile. Node's built-in .env support does not replace @nestjs/config.
  • Validation: class-validator@^0.15 + class-transformer@^0.5 with a global ValidationPipe, or zod@^4.4 + nestjs-zod@^5.4 (ZodValidationPipe). Pick one per project; do not mix DTO styles.
  • Config: @nestjs/config@^4 with a validation schema (Zod or Joi).
  • ORM: Prisma 7 (prisma/@prisma/client ^7.8, TypeScript runtime, no Rust engine) for new work; or TypeORM 1.0 (typeorm@^1.0) if the team already runs it. Both live in the repository/service layer only.
  • Auth: @nestjs/jwt@^11, @nestjs/passport@^11 + passport-jwt, or a custom guard. Hash with argon2@^0.44 (preferred) or bcrypt.
  • Docs: @nestjs/swagger@^11 (OpenAPI 3.2 — call setOpenAPIVersion('3.2.0') if you use hierarchical tags). Logging: nestjs-pino@^4 + pino@^10. Rate limit: @nestjs/throttler@^6. Security headers: helmet@^8.
  • Testing: Jest 30 (jest@^30) via @nestjs/testing, supertest@^7 for e2e, @swc/jest for fast transform.
  • Tooling: @nestjs/cli@^11, ESLint 10 flat config (eslint.config.mjs; eslintrc is removed in v10) + typescript-eslint@^8, Prettier 3, @swc/core for build/watch.

Project conventions

  • File names are kebab-case with a role suffix: users.module.ts, users.controller.ts, users.service.ts, create-user.dto.ts, user.entity.ts, jwt-auth.guard.ts, logging.interceptor.ts, all-exceptions.filter.ts, users.service.spec.ts, users.e2e-spec.ts. One primary class per file.
  • Folder layout: feature-first.
    src/
      main.ts
      app.module.ts
      config/            # registerAs() namespaces + schema
      common/            # guards, interceptors, filters, decorators, pipes (cross-cutting)
      prisma/            # PrismaService (or database/ for TypeORM DataSource)
      users/
        users.module.ts users.controller.ts users.service.ts
        dto/ entities/ users.service.spec.ts
    
  • Generate with schematics: nest g module users, nest g service users. Do not hand-scaffold wiring you can generate.
  • Imports: use the path alias @/* (tsconfig paths + baseUrl) for cross-feature imports; relative paths within a feature. No deep imports into another feature's internals — import via its module's exports.
  • tsconfig: strict: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true, forceConsistentCasingInFileNames: true. Keep emitDecoratorMetadata and experimentalDecorators on (class-validator/TypeORM need them).
  • No any. Use unknown at boundaries and narrow. No // @ts-ignore without a one-line justification and a linked issue.
  • Format/lint are non-negotiable gates: eslint . --max-warnings=0 and prettier --check in CI.

Module structure

  • Feature modules own their surface. A module declares controllers, providers, and exports only the providers other modules legitimately consume. Never export a provider just to make a test pass — inject it in the test module instead.
  • imports for dependencies, exports for the public API. If module B needs A's service, A must export it and B must import: [AModule]. Do not re-provide the same class in two modules (creates two instances).
  • Dynamic modules (forRoot/forRootAsync, forFeature) for configurable infra (DB, cache, config). Mark truly global cross-cutting modules @Global() sparingly (config, Prisma) — overusing @Global() hides the dependency graph.
  • Circular module deps are a design smell, not a forwardRef() opportunity. Break the cycle: extract the shared provider/types into a third module both depend on. Use forwardRef() only as a last resort and leave a comment explaining why.

Dependency injection & scopes

  • Constructor injection with private readonly:
    constructor(private readonly users: UsersService, private readonly config: ConfigService) {}
    
  • Program to tokens for swappable deps: define an abstract class/interface token and provide: MAILER, useClass: SesMailer. Inject with @Inject(MAILER). This keeps services testable without touching concrete infra.
  • Default scope is singleton — keep it. Scope.REQUEST instantiates the provider (and everything that injects it) per request and poisons performance up the chain; use it only when you genuinely need per-request state, and prefer ClsService (nestjs-cls) or the execution context for request data instead.
  • Never do work in a constructor. Use onModuleInit() for async setup (DB connect, warmup). Constructors only assign injected deps.

Controllers stay thin

  • Controllers do routing, DTO binding, and delegation — zero business logic, zero DB access, zero branching on domain state. All of that lives in a service.
    @Post()
    create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
      return this.users.create(dto);
    }
    
  • Return values; do not use @Res() passthrough unless you must stream/set raw headers — grabbing the response object bypasses interceptors, serialization, and the exception filter. If you truly need it, use @Res({ passthrough: true }).
  • Enable global URI versioning (app.enableVersioning({ type: VersioningType.URI })) and version controllers (@Controller({ path: 'users', version: '1' })) rather than baking /v1 into strings.

DTOs & validation — never trust the raw body

  • Register the global ValidationPipe once in main.ts:
    app.useGlobalPipes(new ValidationPipe({
      whitelist: true,              // strip unknown props
      forbidNonWhitelisted: true,   // 400 on unknown props
      transform: true,              // instantiate the DTO class
      transformOptions: { enableImplicitConversion: false },
    }));
    
    Keep enableImplicitConversion: false and convert explicitly with @Type(() => Number) — implicit conversion silently coerces and hides bugs.
  • Every DTO is a class with decorators; the pipe only validates typed class params:
    export class CreateUserDto {
      @IsEmail() email!: string;
      @IsString() @MinLength(12) password!: string;
      @IsOptional() @IsInt() @Min(0) age?: number;
    }
    
  • Validate query and params too (@Query(), @Param() DTOs), not just @Body(). Use @ParseIntPipe/@ParseUUIDPipe for scalar path params.
  • Zod alternative: define a schema, wrap with createZodDto(schema), register ZodValidationPipe globally via APP_PIPE, and call patchNestJsSwagger() for OpenAPI. One validation stack per project.
  • Response DTOs are mandatory — controllers return a mapped response type (or a class with @Expose), never the persistence entity (see below).

Guards, interceptors, filters

  • Guards for authN/authZ. Implement CanActivate, read metadata with Reflector.getAllAndOverride(...) (handler + class). Wire a global auth guard via APP_GUARD and opt public routes out with a @Public() decorator (SetMetadata('isPublic', true)) rather than adding the guard to every controller.
  • Interceptors for cross-cutting response/timing/caching — a TransformInterceptor for the standard envelope, request-timing logs. Use ClassSerializerInterceptor only alongside explicit response DTOs, never as a substitute for mapping.
  • One global exception filter (APP_FILTER) producing a stable error shape:
    { "statusCode": 409, "error": "Conflict", "message": "Email already used", "requestId": "..." }
    
    Map known domain/ORM errors (e.g. Prisma P2002 → 409, P2025 → 404) to HttpExceptions; log unknowns at error with the requestId and return a generic 500 — never leak stack traces or SQL to clients.
  • Register cross-cutting providers with APP_GUARD/APP_INTERCEPTOR/APP_FILTER/APP_PIPE tokens (DI-aware) rather than app.useGlobal* when the component needs injected dependencies.

Configuration

  • ConfigModule.forRoot({ isGlobal: true, validate }) with a schema that fails fast on boot:
    const Env = z.object({
      NODE_ENV: z.enum(['development', 'test', 'production']),
      PORT: z.coerce.number().default(3000),
      DATABASE_URL: z.string().url(),
      JWT_SECRET: z.string().min(32),
    });
    export const validate = (raw: Record<string, unknown>) => Env.parse(raw);
    
  • Namespaced config with registerAs and typed access: configService.get('jwt.secret', { infer: true }) using ConfigService<Config, true>. Do not scatter process.env.X reads through services — read env only in the config layer.
  • Never commit .env. Provide .env.example. Secrets come from the environment/secret manager in prod.

Persistence — entities never cross the controller boundary

  • Prisma 7: one PrismaService extends PrismaClient with onModuleInit()$connect() and onModuleDestroy()$disconnect(); call app.enableShutdownHooks() in main.ts (Prisma removed $on('beforeExit')). Use the prisma-client generator with an explicit output. All queries go through repository/service methods. Use $transaction for multi-write invariants. Raw SQL only via tagged prisma.$queryRaw templates — never string-concatenate SQL.
  • TypeORM 1.0: inject repositories with @InjectRepository(User); synchronize: true is banned outside tests — use generated migrations. Repository access lives in services, not controllers.
  • Map to response DTOs before returning. Persistence models carry secrets (passwordHash), relations, and DB-shaped fields. Return a plain response object/DTO; strip sensitive fields at the mapping step, not by hoping serialization hides them.

Testing

  • Unit tests build an isolated module with Test.createTestingModule({...}).compile() and replace every dependency with a mock via useValue/useClass; assert service logic without touching the DB or HTTP. Resolve with moduleRef.get(UsersService).
  • Override in tests, don't re-export from prod: Test.createTestingModule({...}).overrideProvider(MAILER).useValue(fakeMailer).
  • e2e tests boot the real app graph, apply the same global pipes/filters as main.ts, and drive it with supertest(app.getHttpServer()); assert status codes and response shape, including validation 400s and auth 401/403s. Run e2e with --runInBand.
  • Mock the ORM at the service edge (a repository/PrismaService mock), or run integration tests against a disposable Postgres (Testcontainers). Do not mock deep Prisma internals.
  • Use @swc/jest for transform. Keep jest --coverage thresholds meaningful (branches on services), not a vanity 100%.
  • Always await app.close() in afterAll to release handles.

Security

  • Global input hardening: ValidationPipe with whitelist + forbidNonWhitelisted is your primary defense against mass-assignment. Never disable it "temporarily".
  • helmet() and an explicit CORS allowlist (app.enableCors({ origin: [...] })) — never origin: true in prod. Set a body size limit.
  • Rate-limit with @nestjs/throttler (ThrottlerGuard via APP_GUARD); tighten limits on auth endpoints.
  • Passwords: argon2.hash (or bcrypt cost ≥ 12). Never log, return, or store plaintext. JWT: short-lived access tokens + rotating refresh tokens, pin the algorithm (HS256/RS256), verify exp; secret ≥ 32 chars from config.
  • No SQL string interpolation — rely on the ORM's parameterization; raw queries use tagged templates/parameters only.
  • Don't leak internals: the exception filter returns generic messages for 5xx; Pino redacts authorization, password, token, and cookie headers (redact paths).
  • For cookie/session auth add CSRF protection; prefer stateless bearer tokens for APIs. Keep dependencies patched (pnpm audit/Dependabot).

Do

  • Keep controllers to routing + delegation; put all logic and persistence in services.
  • Validate every @Body/@Query/@Param with a DTO or parse pipe; map DB models to response DTOs before returning.
  • Inject via constructor with private readonly; program to tokens for infra you'll swap or mock.
  • Register cross-cutting concerns with APP_GUARD/APP_INTERCEPTOR/APP_FILTER/APP_PIPE.
  • Fail fast on boot: schema-validate config; app.enableShutdownHooks().
  • Use nest g schematics, path aliases, and one class per file.
  • Write a *.spec.ts next to each service and an e2e test for each controller's happy path plus a validation/authz failure.

Avoid

  • Business logic, DB calls, or try/catch control flow in controllers → move to a service; let the exception filter format errors.
  • new ValidationPipe() with no options, or transform/whitelist disabled → always whitelist + forbidNonWhitelisted + transform.
  • Returning entities/Prisma models directly → map to a response DTO; strip secrets explicitly.
  • Scope.REQUEST by default → keep singletons; use nestjs-cls for request context.
  • forwardRef() to paper over circular deps → extract a shared module.
  • Reading process.env outside the config layer, or an unvalidated config → centralize + schema-validate.
  • synchronize: true (TypeORM) or ad-hoc schema drift → migrations only outside tests.
  • @Res() without passthrough (bypasses interceptors/filters), the deprecated HttpModule from @nestjs/common (use @nestjs/axios), require(), and any.
  • @Global() on every module → reserve for config/Prisma; keep the dependency graph explicit.

When you code

  • Make small, single-purpose diffs. Touch one feature module at a time; don't restructure modules and add features in the same change.
  • After every change run, in order: tsc --noEmit (or nest build), eslint . --max-warnings=0, then the relevant jest/e2e suite. Do not report done until all three pass.
  • New endpoint = DTO(s) + service method + controller wiring + *.spec.ts + an e2e assertion. New provider = added to a module's providers and, if shared, exports.
  • Adding a dependency, a DB migration, or changing the public API of an exported service? State it and ask first — especially anything touching schema, auth, or config validation.
  • Mirror the existing validation stack, error shape, and folder conventions already in the repo; do not introduce a second DTO/validation style. When a rule here conflicts with an established repo convention, flag it rather than silently diverging.

Drop it in your repo

Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on TypeScript 6 · NestJS 11 · Node 24 LTS · Prisma 7.

Back to top ↑