# Nestjs Rules

> Procedural rules and patterns…

- Skill: `codyswanngt/nestjs-rules` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add codyswanngt/nestjs-rules`
- Raw SKILL.md: https://api.skillmd.com/api/skills/codyswanngt/nestjs-rules/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools, Web & Frontend
- Author: codyswanngt (https://skillmd.com/u/codyswanngt)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/codyswanngt/nestjs-rules

---


# 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`

