---
name: nestjs-rules
description: Procedural rules and patterns for NestJS backend development. This skill should be used when creating new NestJS modules, services, resolvers, or controllers. It covers component generation with NestJS CLI, TDD patterns, module structure conventions, Lambda handler patterns, and configuration standards. Use this skill alongside nestjs-graphql for GraphQL-specific patterns.
---

# NestJS Development Rules

## Overview

This skill provides procedural rules for working with NestJS in this project. It covers component generation, testing patterns, module structure, and deployment configuration. For GraphQL-specific patterns (resolvers, types, auth decorators), use the `nestjs-graphql` skill.

## Component Generation

Always use NestJS CLI to create components rather than manually creating files.

### Module Generation

```bash
bunx nest g module <name> --no-spec
```

### Service Generation

```bash
bunx nest g service <name> --no-spec
```

### Resolver Generation (GraphQL)

```bash
bunx nest g resolver <name> --no-spec
```

### Controller Generation (REST)

```bash
bunx nest g controller <name> --no-spec
```

### Why `--no-spec`

The `--no-spec` flag is used because this project follows TDD (Test-Driven Development). Tests are written first with a custom test structure before implementation, not auto-generated by the CLI.

## Database Migrations

### Entity Files Are the Source of Truth

In this project, **entity files (`src/database/entities/*.ts`) are the single source of truth for the database schema**. Migrations are a derived artifact — TypeORM diffs the entity metadata against the current database and emits the migration for you. The workflow is always:

1. Edit the entity file to express the desired schema.
2. Run `bun run migration:generate --name=<DescriptiveName>` to produce the migration from the diff.
3. Review the generated migration, then commit both the entity change and the migration together.

If a schema change cannot be expressed via the entity model, the entity model is wrong — fix the entity, do not hand-write the migration.

### Never Create Migration Files Manually

Never create or modify a TypeORM migration file directly. Use `migration:generate` from `package.json`:

```bash
bun run migration:generate --name=<DescriptiveName>
```

### Out-of-Band Migrations (Seed Data, Backfills)

Some changes genuinely cannot be derived from entity diffs:

- **Seed data** (reference rows, lookup tables, initial admin user)
- **Data backfills** (populating a new column from existing rows)
- **Data transformations** (splitting a column, normalizing values)
- **One-off cleanup** (deleting orphaned rows before a constraint is added)

These are legitimate cases for a hand-written migration, but they are **out-of-band** — they bypass the entity-as-source-of-truth contract. When you encounter one:

1. **Stop and tell the user.** Explain what change is needed and why it cannot be expressed via the entity model.
2. **Get explicit approval** before writing the migration by hand.
3. Document the rationale in the migration's class comment so future readers understand why this one was not generated.

Do not silently hand-write a migration for a backfill or seed-data change. The user must know that the entity-as-source-of-truth contract is being intentionally bypassed for this case.

### Enforcement

The `lisa-nestjs` plugin ships a `PreToolUse` hook (`block-migration-edits.sh`) that blocks `Write`/`Edit` on any path matching `**/migrations/*.ts` or `**/migrations/*.js`. The block surfaces this rule's guidance to remind you to either (a) edit the entity instead, or (b) ask the user before proceeding with an out-of-band migration.

### Why Auto-Generation

TypeORM compares your entity definitions against the current database schema to generate migrations. Manual creation can:
- Introduce drift between entities and migrations
- Miss important schema changes
- Create migrations that don't match entity metadata

## Test-Driven Development Pattern

### Write Tests First

Create test files before implementation:

```typescript
// src/feature/feature.service.test.ts
import { Test, TestingModule } from "@nestjs/testing";
import { FeatureService } from "./feature.service";

describe("FeatureService", () => {
  const service: FeatureService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [FeatureService],
    }).compile();

    service = module.get<FeatureService>(FeatureService);
  });

  describe("methodName", () => {
    it("should do expected behavior", () => {
      expect(service.methodName()).toBe("expected");
    });
  });
});
```

### Test File Naming

- Unit tests: `*.test.ts`
- Integration tests: `*.integration.test.ts`

### Running Tests

```bash
# Unit tests only
bun run test:unit

# Integration tests only
bun run test:integration

# All tests
bun run test
```

## Module Structure

### Feature Module Pattern

```
src/
├── <feature>/
│   ├── <feature>.module.ts        # Module definition
│   ├── <feature>.service.ts       # Business logic
│   ├── <feature>.service.test.ts  # Service unit tests
│   ├── <feature>.resolver.ts      # GraphQL resolver (if applicable)
│   ├── <feature>.resolver.test.ts # Resolver unit tests
│   ├── <feature>.controller.ts    # REST controller (if applicable)
│   ├── <feature>.controller.test.ts
│   ├── dto/                       # Data transfer objects
│   │   ├── create-<feature>.input.ts
│   │   └── update-<feature>.input.ts
│   └── entities/                  # Entity definitions
│       └── <feature>.entity.ts
```

### Module Registration

Register feature modules in `app.module.ts`:

```typescript
import { Module } from "@nestjs/common";
import { FeatureModule } from "./feature/feature.module";

@Module({
  imports: [
    // ... other imports
    FeatureModule,
  ],
})
export class AppModule {}
```

## Lambda Handler Pattern

### Entry Point Structure

```typescript
// src/main.ts
import { NestFactory } from "@nestjs/core";
import { configure as serverlessExpress } from "@vendia/serverless-express";
import { AppModule } from "./app.module";

type ServerlessHandler = ReturnType<typeof serverlessExpress>;

/**
 * Creates a lazy-initialized server getter using closure pattern
 * @description Encapsulates mutable cache state for Lambda warm starts
 * @returns Async function that returns the cached or newly created server
 */
const createServerGetter = (): (() => Promise<ServerlessHandler>) => {
  // eslint-disable-next-line functional/no-let -- Required for Lambda warm start caching
  let cachedServer: ServerlessHandler | null = null;

  return async (): Promise<ServerlessHandler> => {
    if (cachedServer) {
      return cachedServer;
    }

    const nestApp = await NestFactory.create(AppModule, {
      cors: {
        origin: "*",
        methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
        preflightContinue: false,
        optionsSuccessStatus: 204,
      },
    });

    await nestApp.init();
    const app = nestApp.getHttpAdapter().getInstance();
    cachedServer = serverlessExpress({ app });
    return cachedServer;
  };
};

const getServer = createServerGetter();

/**
 * Lambda handler function
 * @param event - AWS Lambda event object
 * @param context - AWS Lambda context object
 * @returns Promise resolving to Lambda response
 */
export const handler = async (
  event: unknown,
  context: unknown
): Promise<unknown> => {
  const server = await getServer();
  return server(event, context);
};
```

### Key Lambda Concepts

1. **Warm Start Caching**: Use closure pattern to cache the NestJS application between invocations
2. **Serverless Express**: Use `@vendia/serverless-express` for Express adapter compatibility
3. **CORS Configuration**: Configure CORS in NestFactory.create(), not in serverless.yml

## Configuration Files

### NestJS CLI Configuration

```json
// nest-cli.json
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "deleteOutDir": true
  }
}
```

### TypeScript Configuration

Required settings for NestJS decorators:

```json
// tsconfig.json (additions)
{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false
  }
}
```

### Serverless Framework Configuration

```yaml
# serverless.yml
service: project-name
frameworkVersion: "^4.0.0"

custom:
  esbuild:
    bundle: true
    minify: false
    sourcemap: true
    keepNames: true
    platform: node
    target: node20
    external:
      - "fsevents"
      - "@nestjs/websockets"
      - "@nestjs/microservices"
      - "@apollo/gateway"
      - "@apollo/subgraph"
      - "@as-integrations/fastify"
      - "class-transformer/storage"

plugins:
  - serverless-esbuild
  - serverless-offline

provider:
  name: aws
  runtime: nodejs22.x
  region: us-east-1
  httpApi:
    cors: true

functions:
  main:
    handler: src/main.handler
    timeout: 29
    memorySize: 1024
    events:
      - httpApi:
          method: any
          path: /{proxy+}
```

## Documentation Standards

### JSDoc for All Components

Every exported function, class, and type must have JSDoc documentation:

```typescript
/**
 * Service for managing user accounts
 * @description Provides CRUD operations for user entities
 * @remarks
 * - All methods are idempotent
 * - Throws NotFoundException for missing resources
 */
@Injectable()
export class UserService {
  /**
   * Retrieves a user by their unique identifier
   * @param id - The unique identifier of the user
   * @returns The user if found, null otherwise
   */
  async findById(id: string): Promise<User | null> {
    return this.repository.findOne({ where: { id } });
  }
}
```

### Module File Preambles

Every file should have a preamble comment:

```typescript
/**
 * @file user.service.ts
 * @description Service providing user account management
 * @module users
 */
```

## DataLoader Integration

### Batch Method Pattern

Services that support DataLoader must implement batch methods:

```typescript
/**
 * Batch loads entities by IDs (for DataLoader)
 * @param ids - Array of entity IDs to load
 * @returns Promise resolving to array of entities in same order as input
 * @remarks Used by DataLoader for batching - maintains input order
 */
async findByIds(ids: readonly string[]): Promise<Entity[]> {
  const entities = await this.repository.findBy({ id: In([...ids]) });
  const entityMap = new Map(entities.map(e => [e.id, e]));
  return ids.map(id => entityMap.get(id) ?? null);
}
```

### Order Preservation

Batch functions must return results in the same order as input keys. Always map input IDs to results to maintain order.

## Common Patterns

### Immutable Code

Use `const` instead of `let` or `var`:

```typescript
// Good
const users = await this.userService.findAll();
const filtered = users.filter(u => u.active);

// Bad
let users = await this.userService.findAll();
users = users.filter(u => u.active);
```

### Functional Transformations

Use `reduce` instead of `push` or `pop`:

```typescript
// Good
const userMap = users.reduce(
  (acc, user) => ({ ...acc, [user.id]: user }),
  {} as Record<string, User>
);

// Bad
const userMap: Record<string, User> = {};
users.forEach(user => {
  userMap[user.id] = user;
});
```

### Error Handling

Use GraphQL errors with codes:

```typescript
import { GraphQLError } from "graphql";

throw new GraphQLError("User not found", {
  extensions: { code: "NOT_FOUND", id },
});
```

## Configuration Management

### Never Use process.env Directly

Always use NestJS ConfigService instead of accessing `process.env` directly. This provides:
- Type safety with full autocomplete
- Centralized configuration management
- Easier testing (mock ConfigService instead of environment)
- Validation at startup

### ConfigService in NestJS Context

For services, resolvers, and controllers running within NestJS:

```typescript
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Configuration } from "../config/configuration";

@Injectable()
export class MyService {
  constructor(
    private readonly configService: ConfigService<Configuration, true>
  ) {}

  someMethod(): void {
    // Type-safe configuration access with autocomplete
    const host = this.configService.get("database.host", { infer: true });
    const isOffline = this.configService.get("app.isOffline", { infer: true });
  }
}
```

### ConfigService in Module Factories

For dynamic module configuration (e.g., TypeOrmModule.forRootAsync):

```typescript
import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Configuration } from "../config/configuration";

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService<Configuration, true>) =>
        createTypeOrmOptionsFromConfigService(configService),
    }),
  ],
})
export class DatabaseModule {}
```

### Standalone Configuration (Lambda Handlers)

For code running outside NestJS context (Lambda authorizers, WebSocket handlers):

```typescript
import { getStandaloneConfig } from "../../config/configuration";

// Use getStandaloneConfig() for type-safe access outside NestJS
const config = getStandaloneConfig();
const host = config.valkey.host;
const port = config.valkey.port;
```

### Configuration Schema

All configuration is defined in `src/config/configuration.ts`:

```typescript
export interface Configuration {
  readonly app: {
    readonly nodeEnv: string;
    readonly isOffline: boolean;
  };
  readonly database: {
    readonly host: string;
    readonly port: number;
    readonly username: string;
    readonly password: string;
    readonly name: string;
    // ... other database config
  };
  readonly valkey: {
    readonly host: string;
    readonly port: number;
    readonly maxRetriesPerRequest: number;
  };
  // ... other configuration namespaces
}
```

### Adding New Configuration

1. Add the new property to the `Configuration` interface
2. Add default values in the `configuration()` factory function
3. Access via `configService.get("namespace.property", { infer: true })`

### Testing with ConfigService

Create mock ConfigService in tests:

```typescript
const createMockConfigService = (): ConfigService<Configuration, true> => {
  const config = {
    valkey: { host: "localhost", port: 6379, maxRetriesPerRequest: 3 },
  };

  return {
    get: jest.fn((key: string) => {
      const keys = key.split(".");
      return keys.reduce((obj, k) => obj?.[k], config);
    }),
  } as unknown as ConfigService<Configuration, true>;
};

// In test setup
const module = await Test.createTestingModule({
  providers: [
    MyService,
    { provide: ConfigService, useValue: createMockConfigService() },
  ],
}).compile();
```

## Verification Checklist

After creating or modifying NestJS components:

1. **Unit tests pass**: `bun run test:unit`
2. **Integration tests pass**: `bun run test:integration`
3. **Linting passes**: `bun run lint`
4. **Type check passes**: `bun run build`
5. **Local server starts**: `bun run start:local`
