Skip to content

Instantly share code, notes, and snippets.

@anhdiepmmk
Last active December 26, 2025 08:35
Show Gist options
  • Select an option

  • Save anhdiepmmk/6384a492ecbbb3aea29545aefd2baffe to your computer and use it in GitHub Desktop.

Select an option

Save anhdiepmmk/6384a492ecbbb3aea29545aefd2baffe to your computer and use it in GitHub Desktop.

ROLE: You are a Senior Frontend Developer.

TECH STACK:

  • TypeScript
  • React
  • Tailwind CSS
  • TanStack Query
  • Redux Toolkit
  • React Router
  • use-query-params
  • shadcn/ui
  • Axios
  • React Hook Form
  • Joi
  • Testing:
    • Unit / Integration: Vitest or Jest + React Testing Library

GENERAL RULES:

  • Follow clean architecture and long-term maintainability.
  • Prefer readability over clever or condensed code.
  • All output must be production-ready.
  • Do not over-engineer.

STYLING:

  • DO NOT write custom CSS files.
  • DO NOT write inline styles.
  • Use ONLY Tailwind utility classes.
  • DO NOT customize Tailwind theme unless explicitly required.
  • Split components instead of creating complex class strings.

COMPONENT DESIGN:

  • Enforce Single Responsibility Principle.
  • Components must be small, focused, and reusable.
  • Split large components aggressively.
  • Strictly separate:
    • Page
    • Feature Component
    • Shared UI
    • Hook
    • API layer
  • Avoid god components.

UI & FRAMEWORK USAGE:

  • ALWAYS prefer existing components from:
    • React Router
    • shadcn/ui
    • Stable, well-maintained npm packages
  • Create custom components ONLY if no suitable library solution exists.

STATE PRIORITY (HIGHEST → LOWEST):

  1. URL STATE

    • Managed ONLY via use-query-params
    • Pagination, filtering, sorting, searching
    • URL is the single source of truth
  2. SERVER STATE

    • Managed ONLY via TanStack Query
    • MUST depend on URL state when applicable
  3. GLOBAL CLIENT STATE

    • Managed ONLY via Redux Toolkit
    • UI state, auth/session metadata
    • MUST NOT duplicate URL or server state
  4. LOCAL COMPONENT STATE

    • Temporary UI-only state
    • MUST NOT duplicate higher-priority state

STATE RULES:

  • NEVER duplicate the same state across layers.
  • NEVER manually sync state between layers.
  • If state can live in URL → it MUST live in URL.
  • Redux MUST NOT store server state.
  • TanStack Query MUST NOT store UI-only state.

ROUTING & URL STATE:

  • Use use-query-params for ALL URL-based state.
  • NEVER use window.location, history, or URLSearchParams manually.

DATA FETCHING:

  • All server state MUST use TanStack Query.
  • NEVER fetch data directly inside components.
  • Query keys MUST include URL parameters when applicable.
  • Queries and mutations MUST be isolated from UI logic.

REDUX TOOLKIT:

  • Redux is ONLY for global client state.
  • Prefer synchronous reducers.
  • Async logic ONLY when unavoidable.
  • Use typed hooks (useAppDispatch, useAppSelector).
  • Redux state MUST be fully typed.

API LAYER:

  • Use a centralized Axios instance.
  • NEVER call axios directly inside components.
  • Each endpoint MUST have its own typed function.
  • API responses MUST be strongly typed.

FORMS & VALIDATION:

  • Use React Hook Form for all forms.
  • Use Joi for schema validation.
  • NEVER manually validate inside components.
  • Form values MUST be fully typed.

==================================== TESTING STRATEGY (MANDATORY)

TEST PYRAMID (MUST FOLLOW):

  1. Unit Tests (MOST)
  2. Integration / Component Tests
  3. UI Automation Tests (FEWEST, optional)

UNIT TESTING (MANDATORY)

PURPOSE:

  • Verify isolated logic correctness.

SCOPE:

  • Pure functions and utilities
  • Custom hooks (isolated)
  • Redux reducers and selectors
  • Business logic helpers

RULES:

  • One behavior per test.
  • Tests must be deterministic and isolated.
  • No shared mutable state.
  • No DOM rendering unless required.

INTEGRATION / COMPONENT TESTING (MANDATORY LAYER)

PURPOSE:

  • Verify collaboration between multiple units.
  • Bridge unit tests and UI automation.

SCOPE:

  • Component + hooks interaction
  • URL state (use-query-params) + component behavior
  • TanStack Query + component rendering (mocked network)
  • Redux state + component behavior
  • Form behavior + validation schemas

RULES:

  • Use React Testing Library.
  • Render real components (NO shallow rendering).
  • Mock ONLY external boundaries:
    • Network requests
    • Browser APIs
  • DO NOT mock internal component logic.
  • Prefer testing user-observable behavior.

WHAT TO TEST:

  • Conditional rendering
  • User interactions
  • State transitions across layers
  • Error and empty states

WHAT NOT TO TEST:

  • Styling or layout
  • Animations
  • Full cross-page navigation
  • Browser-specific behavior

TEST PLACEMENT:

  • Component.tsx → Component.test.tsx
  • Page.tsx → Page.integration.test.tsx

PRIORITY:

  • Fewer than unit tests
  • More than UI automation tests

ULTRA TESTING RULES (STRICT)

TEST PRIORITY ORDER:

  1. Business rules
  2. Edge cases
  3. Error paths
  4. Happy paths

ANTI-PATTERN BLACKLIST (FORBIDDEN):

  • Snapshot testing for logic
  • Shallow rendering
  • Testing implementation details
  • Over-mocking components
  • Tests without assertions
  • Multiple behaviors in one test

TEST DESIGN RULES:

  • Arrange → Act → Assert
  • Black-box testing only
  • Test names MUST describe user-observable behavior

AI SELF-CHECK (MANDATORY BEFORE OUTPUT): Before generating code, verify:

  • Is the logic testable?
  • Are side effects isolated?
  • Can dependencies be mocked cleanly?
  • Does each behavior map to a clear test case?

If NOT testable → refactor before output.


TYPESCRIPT (STRICT)

  • NEVER use any.
  • NEVER use unknown.
  • All data structures MUST be explicitly typed.
  • Strong typing required for:
    • API responses
    • Form data
    • Component props
    • Redux state
    • Test mocks
  • NEVER bypass type safety.

==================================== PROJECT STRUCTURE (MANDATORY)

src/ ├─ app/ ├─ pages/ ├─ components/ ├─ shared/ ├─ api/ ├─ hooks/ ├─ forms/ └─ index.tsx

==================================== OUTPUT CONSTRAINTS

  • Generate ONLY TypeScript + React code.
  • Include unit or integration tests when logic exists.
  • Do NOT explain unless explicitly asked.
  • Do NOT violate any rule.
  • If any rule is violated, regenerate the answer correctly.

Antigravity AI Global Rules

Project Overview

This is a full-stack JavaScript/TypeScript project using Nx monorepo workspace with microservices architecture deployed on Kubernetes.

Tech Stack

Backend

  • Languages: JavaScript, TypeScript, Node.js
  • Frameworks: Hapi (HTTP framework), NestJS
  • Workspace: Nx.dev monorepo

Frontend

  • Frameworks: React (current), AngularJS (legacy)

Infrastructure & Deployment

  • Orchestration: Kubernetes (k8s)
  • Environments: local, dev, staging, prod
  • Tools: Skaffold, Minikube
  • Configuration: YAML manifests

Development Process

Test-Driven Development (TDD)

We follow TDD methodology strictly for all new development:

  1. Red Phase: Write a failing test first
  2. Green Phase: Write minimal code to make the test pass
  3. Refactor Phase: Improve code while keeping tests green

TDD Workflow:

  • Always write tests BEFORE implementation code
  • Start with the simplest test case
  • Write one test at a time
  • Run tests frequently (after each change)
  • Refactor only when tests are passing
  • Keep test code clean and maintainable

Test Coverage Requirements:

  • All new features must have tests written first
  • Aim for >80% code coverage
  • 100% coverage for critical business logic
  • Test both happy paths and edge cases

Code Style & Standards

General Guidelines

  • Use TypeScript for all new code unless working with legacy JavaScript files
  • Follow functional programming patterns where appropriate
  • Prefer async/await over promises chains
  • Use ESM (ES Modules) import/export syntax
  • Keep functions small and focused (single responsibility)
  • Write tests first, then implementation (TDD)

TypeScript Type Annotations - ALWAYS REQUIRED

CRITICAL: Always explicitly declare types for ALL variables and function return types

Type Definition Priority (MUST FOLLOW):

  1. type (HIGHEST PRIORITY - use by default)
  2. class (use for DTOs, entities, models with validation/decoration)
  3. interface (LOWEST PRIORITY - avoid unless specifically needed)
// ✅ CORRECT - Use 'type' by default
type User = {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
};

type UserStatus = 'active' | 'inactive' | 'suspended';

type CreateUserPayload = {
  email: string;
  name: string;
  password: string;
};

// ✅ CORRECT - Use 'class' for DTOs with validation/documentation
import { ApiProperty } from '@nestjs/swagger';
import * as Joi from 'joi';

class CreateUserDto {
  @ApiProperty({ example: 'john@example.com', description: 'User email address' })
  email: string;

  @ApiProperty({ example: 'John Doe', description: 'User full name' })
  name: string;

  @ApiProperty({ example: 'P@ssw0rd', description: 'User password' })
  password: string;

  static readonly schema: Joi.ObjectSchema = Joi.object({
    email: Joi.string().email().required(),
    name: Joi.string().min(3).max(50).required(),
    password: Joi.string().min(8).required(),
  });
}

class UpdateUserDto {
  @ApiProperty({ example: 'John Smith', required: false })
  name?: string;

  @ApiProperty({ example: 'active', enum: ['active', 'inactive', 'suspended'], required: false })
  status?: string;

  static readonly schema: Joi.ObjectSchema = Joi.object({
    name: Joi.string().min(3).max(50).optional(),
    status: Joi.string().valid('active', 'inactive', 'suspended').optional(),
  });
}

// ✅ CORRECT - Use 'class' for entities/models
class UserEntity {
  id: string;
  email: string;
  name: string;
  status: UserStatus;
  createdAt: Date;
  updatedAt: Date;

  constructor(data: Partial<UserEntity>) {
    Object.assign(this, data);
  }
}

// ❌ AVOID - Don't use 'interface' (low priority)
interface IUser {  // AVOID - use 'type' instead
  id: string;
  email: string;
}

// ❌ WRONG - Type inference not allowed
const name = "John";
const age = 25;
const isActive = true;
const items = [1, 2, 3];
let count = 0;

// ❌ WRONG - Missing return type
function getUser(id: string) {
  return findUserById(id);
}

// ✅ CORRECT - Explicit type annotations required
const name: string = "John";
const age: number = 25;
const isActive: boolean = true;
const items: number[] = [1, 2, 3];
let count: number = 0;

// ✅ CORRECT - Return type declared
function getUser(id: string): User {
  const user: User = findUserById(id);
  return user;
}

// ✅ CORRECT - Complex types
const user: User = { id: '123', name: 'John', email: 'john@example.com', createdAt: new Date() };
const users: User[] = [user];
const config: Record<string, string> = { api: 'https://api.example.com' };
const callback: (data: string) => void = (data: string): void => console.log(data);

// ✅ CORRECT - Arrow functions with return types
const calculateTotal = (items: Item[]): number => {
  const total: number = _.sumBy(items, 'price');
  return total;
};

const getUserById = async (id: string): Promise<User> => {
  const user: User = await repository.findById(id);
  return user;
};

// ✅ CORRECT - Void return type
function logMessage(message: string): void {
  console.log(message);
}

const handleClick = (): void => {
  console.log('clicked');
};

// ✅ CORRECT - Promise return types
async function fetchUser(id: string): Promise<User> {
  const response: Response = await fetch(`/api/users/${id}`);
  const user: User = await response.json();
  return user;
}

// ✅ CORRECT - Even for obvious types
const PI: number = 3.14159;
const API_URL: string = 'https://api.example.com';
const MAX_RETRIES: number = 3;
const IS_PRODUCTION: boolean = process.env.NODE_ENV === 'production';

// ✅ CORRECT - Generic return types
function mapToIds<T extends { id: string }>(items: T[]): string[] {
  const ids: string[] = _.map(items, 'id');
  return ids;
}

Type Annotation Rules:

  1. Use 'type' by default for all type definitions
  2. Use 'class' for DTOs with Joi validation and Swagger decorators
  3. Use 'class' for entities/models that need methods or constructors
  4. AVOID 'interface' unless specifically required (low priority)
  5. ALL const declarations must have explicit type: const x: string = "value"
  6. ALL let declarations must have explicit type: let count: number = 0
  7. ALL function parameters must have types: function fn(param: string): void
  8. ALL function return types must be declared: function fn(): string { return ""; }
  9. Even when type is obvious, still declare it: const num: number = 5
  10. Arrow functions must declare return types: const fn = (): number => 5
  11. Async functions must declare Promise return types: async function fn(): Promise<User> {}
  12. Void functions must declare void: function log(): void {}

Naming Conventions

  • Files: kebab-case (e.g., user-service.ts, auth-controller.ts)
  • Classes: PascalCase (e.g., UserService, AuthController)
  • Functions/Variables: camelCase (e.g., getUserById, isAuthenticated)
  • Constants: UPPER_SNAKE_CASE (e.g., MAX_RETRY_COUNT, API_BASE_URL)
  • Interfaces/Types: PascalCase with 'I' prefix for interfaces (e.g., IUserData, AuthToken)

Backend Patterns

Common Libraries & Standards

  • Mock Creation: Always use @golevelup/ts-jest createMock
  • Date/Time: Always use moment with UTC (moment.utc())
  • Validation: Always use joi for schema validation
  • Array/Object Operations: Always use lodash (_) for safety - never use native array methods
  • Type Declarations: ALWAYS explicitly declare types for ALL variables (const, let, function parameters, return types)

CRITICAL: Type Safety Rules

// ❌ NEVER use 'as' or 'any' type assertions
const mock = { method: jest.fn() } as any; // FORBIDDEN
const data = {} as User; // FORBIDDEN

// ❌ NEVER omit type declarations
const name = "John"; // FORBIDDEN
let count = 0; // FORBIDDEN
function getUser(id: string) { } // FORBIDDEN - missing return type

// ✅ ALWAYS use proper typing and explicit declarations
import { createMock } from '@golevelup/ts-jest';
const mock = createMock<Service>({ method: jest.fn() });
const data = createMock<User>({ id: '123', name: 'John' });
const name: string = "John"; // REQUIRED
let count: number = 0; // REQUIRED
function getUser(id: string): User { } // REQUIRED - return type declared

Date/Time Handling

import * as moment from 'moment';

// ✅ ALWAYS use moment.utc() with explicit types
const now: moment.Moment = moment.utc();
const tomorrow: moment.Moment = moment.utc().add(1, 'day');
const formatted: string = moment.utc().format('YYYY-MM-DD HH:mm:ss');

// ❌ NEVER use native Date
const now = new Date(); // FORBIDDEN

Validation with Joi

import * as Joi from 'joi';

const schema: Joi.ObjectSchema = Joi.object({
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(18),
  createdAt: Joi.date().iso(),
});

const { error, value } = schema.validate(data);
if (error) {
  throw new Error(error.message);
}

Array/Object Operations with Lodash

import * as _ from 'lodash';

// ✅ ALWAYS use lodash with explicit return types
const filtered: User[] = _.filter(users, { status: 'active' });
const mapped: string[] = _.map(users, 'name');
const grouped: _.Dictionary<User[]> = _.groupBy(users, 'department');
const sorted: User[] = _.orderBy(users, ['createdAt'], ['desc']);
const found: User | undefined = _.find(users, { id: '123' });
const unique: User[] = _.uniqBy(users, 'email');

// ❌ NEVER use native array methods
const filtered = users.filter(u => u.status === 'active'); // FORBIDDEN
const mapped = users.map(u => u.name); // FORBIDDEN

Hapi Framework

  • Use route handlers with request/reply pattern
  • Implement validation using Joi schemas
  • Structure routes in separate modules
  • Use plugins for cross-cutting concerns
  • Handle errors with Boom

NestJS

  • Use decorators for controllers, services, modules
  • Implement dependency injection via constructor
  • Follow module-based architecture
  • Use DTOs (class-based) for request/response validation with Joi and Swagger decorators
  • Implement guards for authentication/authorization
  • Use interceptors for cross-cutting concerns

NestJS DTO Pattern:

import { ApiProperty } from '@nestjs/swagger';
import * as Joi from 'joi';

// ✅ CORRECT - DTOs as classes with Joi validation and Swagger decorators
export class CreateProductDto {
  @ApiProperty({ 
    example: 'Laptop Pro 15', 
    description: 'Product name',
    required: true 
  })
  name: string;

  @ApiProperty({ 
    example: 1299.99, 
    description: 'Product price in USD',
    minimum: 0,
    required: true 
  })
  price: number;

  @ApiProperty({ 
    example: 'electronics', 
    description: 'Product category',
    enum: ['electronics', 'clothing', 'food'],
    required: true 
  })
  category: string;

  @ApiProperty({ 
    example: 'High-performance laptop', 
    description: 'Product description',
    required: false 
  })
  description?: string;

  // Joi schema for validation
  static readonly schema: Joi.ObjectSchema = Joi.object({
    name: Joi.string().min(3).max(100).required(),
    price: Joi.number().positive().precision(2).required(),
    category: Joi.string().valid('electronics', 'clothing', 'food').required(),
    description: Joi.string().max(500).optional(),
  });
}

export class UpdateProductDto {
  @ApiProperty({ example: 'Laptop Pro 16', required: false })
  name?: string;

  @ApiProperty({ example: 1399.99, minimum: 0, required: false })
  price?: number;

  @ApiProperty({ 
    enum: ['electronics', 'clothing', 'food'], 
    required: false 
  })
  category?: string;

  static readonly schema: Joi.ObjectSchema = Joi.object({
    name: Joi.string().min(3).max(100).optional(),
    price: Joi.number().positive().precision(2).optional(),
    category: Joi.string().valid('electronics', 'clothing', 'food').optional(),
  });
}

// Controller usage
@Controller('products')
@ApiTags('products')
export class ProductController {
  constructor(private readonly productService: ProductService) {}

  @Post()
  @ApiOperation({ summary: 'Create a new product' })
  @ApiResponse({ status: 201, description: 'Product created successfully' })
  @ApiResponse({ status: 400, description: 'Invalid input' })
  async create(@Body() createProductDto: CreateProductDto): Promise<Product> {
    // Validate with Joi
    const { error, value } = CreateProductDto.schema.validate(createProductDto);
    if (error) {
      throw new BadRequestException(error.message);
    }
    
    const product: Product = await this.productService.create(value);
    return product;
  }

  @Patch(':id')
  @ApiOperation({ summary: 'Update a product' })
  async update(
    @Param('id') id: string,
    @Body() updateProductDto: UpdateProductDto,
  ): Promise<Product> {
    const { error, value } = UpdateProductDto.schema.validate(updateProductDto);
    if (error) {
      throw new BadRequestException(error.message);
    }
    
    const product: Product = await this.productService.update(id, value);
    return product;
  }
}

Nx Workspace

  • Each app/library should have clear boundaries
  • Use @nx/ scoped imports for workspace libraries
  • Follow the project.json configuration pattern
  • Use tags for enforcing module boundaries
  • Keep shared code in libs, apps should be thin

Frontend Patterns

React

  • Use functional components with hooks
  • Prefer TypeScript interfaces for props
  • Use React Query or SWR for data fetching
  • Implement error boundaries
  • Keep components small and composable
  • Use proper key props in lists

AngularJS (Legacy)

  • Maintain existing patterns when modifying
  • Don't mix Angular and AngularJS code
  • Plan migration path to modern framework
  • Document any legacy-specific workarounds

Kubernetes & Deployment

Environment Configuration

  • Use separate YAML manifests per environment
  • Store secrets in Kubernetes secrets, never in code
  • Use ConfigMaps for non-sensitive configuration
  • Follow 12-factor app principles

Skaffold

  • Local development uses skaffold dev for hot reload
  • Each service should have proper health checks
  • Use resource limits and requests
  • Implement graceful shutdown handlers

Best Practices

  • Always specify resource limits in k8s manifests
  • Use readiness and liveness probes
  • Implement proper logging (structured JSON logs)
  • Use namespaces for environment separation
  • Tag images properly (never use :latest in prod)

Testing Requirements

TDD Test Structure

Follow this pattern for all new code:

import { createMock } from '@golevelup/ts-jest';
import * as moment from 'moment';

// Example: user-service.spec.ts
describe('UserService', () => {
  let userService: UserService;
  let mockRepository: jest.Mocked<UserRepository>;
  let mockEmailService: jest.Mocked<EmailService>;

  beforeEach(() => {
    // CORRECT: Use createMock<T> - NO 'as any' allowed
    mockRepository = createMock<UserRepository>({
      save: jest.fn(),
      findById: jest.fn(),
    });
    
    mockEmailService = createMock<EmailService>({
      sendWelcomeEmail: jest.fn(),
    });
    
    userService = new UserService(mockRepository, mockEmailService);
  });

  // CRITICAL: Always use '#' prefix for method/function names in describe blocks
  describe('#createUser', () => {
    it('should create a user with valid data', async () => {
      // Arrange
      const now: moment.Moment = moment.utc(); // Always use UTC
      const userData: CreateUserDto = { 
        email: 'test@example.com', 
        name: 'Test User',
        createdAt: now.toDate(),
      };
      
      const savedUser: User = createMock<User>({
        id: '123',
        email: userData.email,
        name: userData.name,
        createdAt: now.toDate(),
      });
      
      mockRepository.save.mockResolvedValue(savedUser);

      // Act
      const result: User = await userService.createUser(userData);

      // Assert - Data equality
      expect(result).toEqual(savedUser);
      expect(result.email).toBe('test@example.com');
      expect(result.name).toBe('Test User');
      expect(moment.utc(result.createdAt).isSame(now, 'second')).toBe(true);
      
      // Assert - Function calls
      expect(mockRepository.save).toHaveBeenCalled();
      expect(mockRepository.save).toHaveBeenCalledTimes(1);
      expect(mockRepository.save).toHaveBeenCalledWith(userData);
      
      expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalled();
      expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(savedUser.email);
    });

    it('should throw error when email is invalid', async () => {
      // Arrange
      const userData: CreateUserDto = { 
        email: 'invalid', 
        name: 'Test User',
        createdAt: moment.utc().toDate(),
      };

      // Act & Assert
      await expect(userService.createUser(userData)).rejects.toThrow('Invalid email');
      
      // Assert - Functions NOT called due to early throw
      expect(mockRepository.save).not.toHaveBeenCalled();
      expect(mockEmailService.sendWelcomeEmail).not.toHaveBeenCalled();
    });

    it('should handle repository errors gracefully', async () => {
      // Arrange
      const userData: CreateUserDto = { 
        email: 'test@example.com', 
        name: 'Test User',
        createdAt: moment.utc().toDate(),
      };
      mockRepository.save.mockRejectedValue(new Error('Database error'));

      // Act & Assert
      await expect(userService.createUser(userData)).rejects.toThrow('Database error');
      
      // Assert - Save was called but email was not sent due to error
      expect(mockRepository.save).toHaveBeenCalledTimes(1);
      expect(mockEmailService.sendWelcomeEmail).not.toHaveBeenCalled();
    });
  });

  describe('#updateUserStatus', () => {
    it('should call different methods based on status', async () => {
      // Arrange
      const userId: string = '123';
      const existingUser: User = createMock<User>({
        id: userId,
        status: 'pending',
        updatedAt: moment.utc().toDate(),
      });
      
      mockRepository.findById.mockResolvedValue(existingUser);

      // Act
      await userService.updateUserStatus(userId, 'active');

      // Assert - Verify call order and arguments
      expect(mockRepository.findById).toHaveBeenNthCalledWith(1, userId);
      expect(mockRepository.save).toHaveBeenNthCalledWith(1, 
        expect.objectContaining({ id: userId, status: 'active' })
      );
      expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledTimes(1);
    });

    it('should not send email when status is not active', async () => {
      // Arrange
      const userId: string = '123';
      const existingUser: User = createMock<User>({
        id: userId,
        status: 'pending',
      });
      
      mockRepository.findById.mockResolvedValue(existingUser);

      // Act
      await userService.updateUserStatus(userId, 'suspended');

      // Assert - If/else logic verification
      expect(mockRepository.save).toHaveBeenCalled();
      expect(mockEmailService.sendWelcomeEmail).not.toHaveBeenCalled(); // Not called for suspended status
    });
  });

  describe('#filterActiveUsers', () => {
    it('should filter active users using lodash', () => {
      // Arrange
      const users: User[] = [
        createMock<User>({ id: '1', status: 'active' }),
        createMock<User>({ id: '2', status: 'inactive' }),
        createMock<User>({ id: '3', status: 'active' }),
      ];

      // Act
      const result: User[] = userService.filterActiveUsers(users);

      // Assert
      expect(result).toHaveLength(2);
      expect(result[0].id).toBe('1');
      expect(result[1].id).toBe('3');
    });
  });
});

Test Structure & Naming Conventions

describe() Block Naming:

// ✅ CORRECT - Class/Service name (no prefix)
describe('UserService', () => {
  
  // ✅ CORRECT - Method/Function names MUST have '#' prefix
  describe('#createUser', () => { });
  describe('#updateUser', () => { });
  describe('#deleteUser', () => { });
  describe('#findById', () => { });
  
});

// ✅ CORRECT - For functions
describe('utils', () => {
  describe('#calculateTotal', () => { });
  describe('#formatDate', () => { });
});

// ❌ WRONG - Missing '#' prefix for methods/functions
describe('createUser', () => { }); // FORBIDDEN
describe('calculateTotal', () => { }); // FORBIDDEN

it() Block Naming:

// ✅ CORRECT - Descriptive behavior
it('should create a user with valid data', () => { });
it('should throw error when email is invalid', () => { });
it('should not call service when validation fails', () => { });
it('should return empty array when no users found', () => { });

Unit Test Best Practices

CRITICAL RULES - NEVER VIOLATE:

  1. NO 'as' or 'any' Types in Tests

    // ❌ WRONG - Never do this
    const mock = { save: jest.fn() } as any;
    const user = {} as User;
    
    // ✅ CORRECT - Always use createMock
    const mock = createMock<UserRepository>({
      save: jest.fn(),
    });
    const user = createMock<User>({
      id: '123',
      email: 'test@example.com',
    });
  2. Mock Creation

    import { createMock } from '@golevelup/ts-jest';
    
    // Always use createMock<T> for type safety
    const mockService = createMock<MyService>({
      method: jest.fn(),
    });
    
    const mockData = createMock<DataType>({
      field: 'value',
    });
  3. Date/Time Handling

    import * as moment from 'moment';
    
    // ✅ ALWAYS use moment with UTC
    const now = moment.utc();
    const tomorrow = moment.utc().add(1, 'day');
    const lastWeek = moment.utc().subtract(7, 'days');
    
    // ❌ NEVER use native Date directly
    // const now = new Date(); // WRONG
    
    // Format for comparisons
    const dateStr = moment.utc().format('YYYY-MM-DD');
    
    // Compare dates
    const isSame = moment.utc(date1).isSame(date2, 'day');
    const isAfter = moment.utc(date1).isAfter(date2);
  4. Validation with Joi

    import * as Joi from 'joi';
    
    const userSchema = Joi.object({
      email: Joi.string().email().required(),
      name: Joi.string().min(3).max(50).required(),
      age: Joi.number().integer().min(18).max(120),
      createdAt: Joi.date().iso(),
    });
    
    // In tests
    it('should validate user data', () => {
      const userData = createMock<CreateUserDto>({
        email: 'test@example.com',
        name: 'John',
      });
      
      const { error, value } = userSchema.validate(userData);
      expect(error).toBeUndefined();
      expect(value).toMatchObject(userData);
    });
  5. Array/Object Operations - Always Use Lodash

    import * as _ from 'lodash';
    
    // ✅ CORRECT - Use lodash for safety
    const activeUsers = _.filter(users, { status: 'active' });
    const userNames = _.map(users, 'name');
    const grouped = _.groupBy(users, 'department');
    const sorted = _.orderBy(users, ['createdAt'], ['desc']);
    const unique = _.uniqBy(users, 'email');
    const found = _.find(users, { id: '123' });
    
    // ❌ WRONG - Don't use native array methods
    // const activeUsers = users.filter(u => u.status === 'active'); // NO
    // const userNames = users.map(u => u.name); // NO

Unit Test Assertion Patterns

Data Equality Assertions:

// Exact equality
expect(result).toBe(5);
expect(result).toEqual({ id: '123', name: 'John' });

// Partial object matching
expect(user).toMatchObject({ email: 'test@example.com' });
expect(result).toEqual(expect.objectContaining({ id: expect.any(String) }));

// Array assertions
expect(users).toHaveLength(3);
expect(users).toContain(expectedUser);
expect(users).toEqual(expect.arrayContaining([user1, user2]));

Function Call Assertions:

// Basic call verification
expect(mockFunction).toHaveBeenCalled();
expect(mockFunction).not.toHaveBeenCalled();

// Call count
expect(mockFunction).toHaveBeenCalledTimes(1);
expect(mockFunction).toHaveBeenCalledTimes(3);

// Called with specific arguments
expect(mockFunction).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFunction).toHaveBeenCalledWith(
  expect.any(String),
  expect.objectContaining({ id: '123' })
);

// Nth call verification
expect(mockFunction).toHaveBeenNthCalledWith(1, 'first call arg');
expect(mockFunction).toHaveBeenNthCalledWith(2, 'second call arg');
expect(mockFunction).toHaveBeenNthCalledWith(3, 'third call arg');

// Last call verification
expect(mockFunction).toHaveBeenLastCalledWith('last arg');

If/Else and Control Flow Testing:

// Test both branches
describe('conditional logic', () => {
  it('should call functionA when condition is true', () => {
    service.process(true);
    expect(mockFunctionA).toHaveBeenCalled();
    expect(mockFunctionB).not.toHaveBeenCalled();
  });

  it('should call functionB when condition is false', () => {
    service.process(false);
    expect(mockFunctionA).not.toHaveBeenCalled();
    expect(mockFunctionB).toHaveBeenCalled();
  });
});

// Early return/throw verification
it('should not proceed when validation fails', async () => {
  await expect(service.process(invalidData)).rejects.toThrow();
  expect(mockRepository.save).not.toHaveBeenCalled();
  expect(mockNotificationService.send).not.toHaveBeenCalled();
});

Unit Tests

  • Write tests FIRST before implementation (TDD)
  • Use Jest as test runner
  • Follow Arrange-Act-Assert (AAA) pattern
  • Test file naming: *.spec.ts or *.test.ts
  • Mock all external dependencies (databases, APIs, services)
  • Aim for >80% code coverage, 100% for critical logic
  • NEVER use 'as' or 'any' - always use createMock
  • ALWAYS use moment.utc() for dates
  • ALWAYS use Joi for validation
  • ALWAYS use lodash for array/object operations

What to Assert:

  1. Data equality - Verify returned values match expectations
  2. Function calls - Verify dependencies were called correctly
  3. Call counts - Verify functions called expected number of times
  4. Call arguments - Verify functions called with correct parameters
  5. Call order - Use toHaveBeenNthCalledWith() for sequence verification
  6. Negative cases - Verify functions NOT called in else branches or early returns
  7. Error handling - Verify proper error throwing and handling

Integration Tests

  • Write after unit tests are passing
  • Test API endpoints with supertest
  • Test database operations with test containers
  • Verify service-to-service communication
  • Use separate test databases
  • Clean up test data after each test

E2E Tests

  • Test complete user workflows
  • Run against staging environment
  • Verify Kubernetes deployments
  • Test frontend-backend integration

Testing Best Practices (TDD)

  • Write the test name first (describes expected behavior)
  • Keep tests isolated and independent
  • Use descriptive test names (should/when/given)
  • Don't test implementation details
  • Test behavior, not implementation
  • Refactor tests along with production code
  • Run full test suite before committing

Error Handling

  • Always handle errors explicitly
  • Use custom error classes with proper error codes
  • Log errors with context (correlation IDs)
  • Return consistent error response format
  • Don't expose internal errors to clients

Security Guidelines

  • Validate all input data
  • Sanitize user inputs
  • Use parameterized queries (avoid SQL injection)
  • Implement rate limiting
  • Use HTTPS in all non-local environments
  • Keep dependencies updated
  • Never commit secrets or API keys

Performance Considerations

  • Use connection pooling for databases
  • Implement caching strategies (Redis)
  • Optimize database queries (add indexes)
  • Use CDN for static assets
  • Implement pagination for large datasets
  • Use streaming for large file transfers

Documentation

  • Add JSDoc comments for public APIs
  • Document complex business logic
  • Keep README files updated in each package
  • Document environment variables needed
  • Include architecture diagrams in docs folder

AI Assistance Preferences

When Suggesting Code (TDD Approach)

IMPORTANT: Always follow TDD - tests come first!

  1. First: Provide the test(s)
  2. Then: Provide the implementation
  3. Finally: Suggest refactoring if needed

Example Response Format:

// Step 1: Write the test (RED)
describe('utils', () => {
  describe('#calculateTotal', () => {
    it('should sum array of numbers', () => {
      const numbers: number[] = [1, 2, 3];
      const result: number = calculateTotal(numbers);
      expect(result).toBe(6);
    });
  });
});

// Step 2: Minimal implementation (GREEN)
function calculateTotal(numbers: number[]): number {
  const total: number = _.sum(numbers); // Use lodash, not native
  return total;
}

// Step 3: Refactor (if needed)
// Implementation is already clean, no refactoring needed

Code Suggestion Guidelines:

  • Always show tests before implementation
  • ALWAYS use explicit type declarations for ALL variables
  • Use TypeScript types (never 'as' or 'any')
  • Include error handling
  • Add relevant comments for complex logic
  • Consider the Nx workspace structure
  • Include both unit and integration tests when relevant
  • Show the TDD cycle: Red → Green → Refactor
  • Use lodash for all array/object operations
  • Use moment.utc() for all dates
  • Use createMock for all test mocks

When Creating New Features

Follow this TDD workflow:

  1. Write failing test for simplest case
  2. Write minimal code to pass
  3. Write test for next case
  4. Refactor when tests pass
  5. Repeat until feature complete

When Debugging Issues

  • Start by writing a failing test that reproduces the bug
  • Fix the implementation
  • Verify the test now passes
  • Add additional tests for edge cases

When Refactoring

  • Ensure all existing tests pass first
  • Don't change tests and code simultaneously
  • Refactor in small steps
  • Run tests after each change
  • Keep tests green throughout

When Reviewing Code

  • Check for type safety issues
  • Verify proper error handling
  • Ensure consistency with existing patterns
  • Flag security concerns
  • Suggest performance improvements

When Creating New Files

  • Follow the Nx workspace structure
  • Include proper imports and exports
  • Add basic tests
  • Include necessary configuration files
  • Update relevant documentation

When Working with Kubernetes

  • Provide complete YAML manifests
  • Include resource limits
  • Add health check probes
  • Consider all environments (local/dev/staging/prod)
  • Include skaffold.yaml updates if needed

Common Commands

# Nx Commands
nx serve <app-name>
nx test <app-name>                    # Run tests
nx test <app-name> --watch            # TDD mode (watch for changes)
nx test <app-name> --coverage         # Generate coverage report
nx affected:test                       # Test affected projects
nx build <app-name>
nx dep-graph

# TDD Workflow Commands
npm test -- --watch                    # Watch mode for TDD
npm test -- --coverage                 # Check coverage
npm test -- --verbose                  # Detailed test output

# Skaffold Commands
skaffold dev --port-forward
skaffold run -p dev
skaffold delete

# Kubernetes Commands
kubectl get pods -n <namespace>
kubectl logs -f <pod-name>
kubectl describe pod <pod-name>
kubectl apply -f <manifest.yaml>
minikube start

File Structure Convention

/
├── apps/
│   ├── api-gateway/          # Hapi or NestJS gateway
│   ├── user-service/         # NestJS microservice
│   └── frontend/             # React app
├── libs/
│   ├── shared/
│   │   ├── types/           # Shared TypeScript types
│   │   ├── utils/           # Common utilities
│   │   └── constants/       # Shared constants
│   └── domain/
│       └── user/            # Domain-specific logic
├── k8s/
│   ├── base/                # Base k8s configs
│   ├── local/               # Local overrides
│   ├── dev/                 # Dev environment
│   ├── staging/             # Staging environment
│   └── prod/                # Production configs
├── skaffold.yaml
└── nx.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment