# NestJS SDK Implementation Guide

## Overview

### Purpose
Team coding standards for creating production-ready external service integration SDKs in NestJS projects.

### When to Apply
- **When BE System Design requires external API integration (MANDATORY)**
  - External API calls must be encapsulated in SDK modules
  - Direct HTTP requests from business logic are prohibited
- Creating new external API integrations
- Refactoring existing SDK implementations
- Code review for SDK-related PRs

## Architecture Requirements

### Authentication (Strategy Pattern)
```typescript
interface AuthConfig {
  type: 'bearer' | 'basic' | 'apikey';
  credentials: Record<string, string>;
}

private applyAuthentication(config: AxiosRequestConfig): AxiosRequestConfig {
  switch (this.authConfig.type) {
    case 'bearer':
      config.headers.Authorization = `Bearer ${this.authConfig.credentials.token}`;
      break;
    case 'basic':
      config.headers.Authorization = `Basic ${Buffer.from(
        `${this.authConfig.credentials.username}:${this.authConfig.credentials.password}`
      ).toString('base64')}`;
      break;
    case 'apikey':
      config.headers['X-API-Key'] = this.authConfig.credentials.apiKey;
      break;
  }
  return config;
}
```

### HTTP Client Configuration
- Use Axios with request/response interceptors
- Configure timeout from environment variables
- Apply authentication via interceptors

## Response Transformation Rules

### Mandatory Requirements

1. **CamelCase Conversion**: All response properties must be converted to camelCase
2. **Date Parsing**: String dates must be converted to Date objects
3. **Separate Interfaces**: Define internal response interface (transformed) separate from external API response

### Interface Pattern

```typescript
// Raw interface - matches external API response exactly
interface ExternalUserResponse {
  user_id: string;
  user_name: string;
  created_at: string;
  updated_at: string;
  is_active: boolean;
}

// Transformed interface - internal application use
interface UserResponse {
  userId: string;
  userName: string;
  createdAt: Date;
  updatedAt: Date;
  isActive: boolean;
}
```

### Transformer Implementation

```typescript
private transformUserResponse(raw: ExternalUserResponse): UserResponse {
  return {
    userId: raw.user_id,
    userName: raw.user_name,
    createdAt: new Date(raw.created_at),
    updatedAt: new Date(raw.updated_at),
    isActive: raw.is_active,
  };
}
```

## Implementation Checklist

### P0: Required
- [ ] Multi-auth support (bearer, basic, apikey)
- [ ] Response transformation (camelCase + Date)
- [ ] Separate Raw/Transformed response interfaces
- [ ] Retry logic with exponential backoff
- [ ] Structured logging with request/response details
- [ ] Error mapping to NestJS exceptions
- [ ] NestJS module with proper DI
- [ ] Configuration validation at startup

### P1: Recommended
- [ ] Response caching (GET operations only, with TTL)
- [ ] Request/response interceptors for monitoring
- [ ] Unit tests covering error scenarios
- [ ] Input/output validation using class-validator

### P2: Optional
- [ ] Metrics collection (Prometheus/custom)
- [ ] Rate limiting protection

## Code Standards

### SDK Class Structure

```typescript
@Injectable()
export class PaymentSdk {
  private readonly client: AxiosInstance;
  private readonly logger = new Logger(PaymentSdk.name);

  constructor(private readonly configService: ConfigService) {
    this.client = this.createHttpClient();
    this.setupInterceptors();
  }

  async getTransaction(id: string): Promise<TransactionResponse | null> {
    try {
      const response = await this.withRetry(() =>
        this.client.get<ExternalTransactionResponse>(`/transactions/${id}`)
      );
      this.logger.log(`GET /transactions/${id} - ${response.status}`);
      return this.transformTransactionResponse(response.data);
    } catch (error) {
      if (error.response?.status === 404) return null;
      this.logger.error(`Failed to get transaction ${id}:`, error.message);
      throw this.transformError(error);
    }
  }

  private async withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
    for (let i = 0; i < attempts; i++) {
      try {
        return await fn();
      } catch (error) {
        if (i === attempts - 1) throw error;
        if (!this.isRetryable(error)) throw error;
        await this.delay(Math.pow(2, i) * 1000);
      }
    }
    throw new Error('Retry failed');
  }

  private isRetryable(error: any): boolean {
    const status = error.response?.status;
    return status === 429 || (status >= 500 && status < 600);
  }

  private transformTransactionResponse(raw: ExternalTransactionResponse): TransactionResponse {
    return {
      transactionId: raw.transaction_id,
      amount: raw.amount,
      createdAt: new Date(raw.created_at),
      status: raw.status,
    };
  }

  private transformError(error: any): Error {
    // Map to NestJS exceptions
  }
}
```

### Module Configuration

```typescript
export const PAYMENT_SDK = Symbol('PAYMENT_SDK');

@Module({
  providers: [
    {
      provide: PAYMENT_SDK,
      useFactory: (config: ConfigService) => new PaymentSdk(config),
      inject: [ConfigService],
    },
  ],
  exports: [PAYMENT_SDK],
})
export class PaymentSdkModule {}
```

### Configuration Schema

```typescript
export default registerAs('payment', () => ({
  auth: {
    type: process.env.PAYMENT_AUTH_TYPE || 'bearer',
    apiKey: process.env.PAYMENT_API_KEY,
  },
  baseUrl: process.env.PAYMENT_BASE_URL,
  timeout: parseInt(process.env.PAYMENT_TIMEOUT) || 30000,
  retry: {
    attempts: parseInt(process.env.PAYMENT_RETRY_ATTEMPTS) || 3,
    baseDelay: parseInt(process.env.PAYMENT_RETRY_DELAY) || 1000,
  },
}));
```

## Error Handling Rules

| Status | Action | Example |
|--------|--------|---------|
| 404 | return null | Resource not found |
| 429 | retry with backoff | Rate limited |
| 5xx | retry with backoff | Server error |
| 401, 403 | throw immediately | Auth failure |
| Others | transform to NestJS exception | Client error |

## Logging Standards

### Request/Response Format
```typescript
this.logger.log(`${method} ${url} - ${status} (${duration}ms)`);
```

### Error Context Requirements
```typescript
this.logger.error(`Operation failed: ${operation}`, {
  input,
  error: error.message,
  status: error.response?.status,
});
```

## Prohibited Practices

### Don't
- Hardcode secrets or configuration values
- Skip error handling for external calls
- Use `any` types without validation
- Make requests without retry logic
- Return raw API responses without transformation
- Mix snake_case and camelCase in response interfaces

### Do
- Use ConfigService with environment validation
- Implement comprehensive error handling
- Define strict TypeScript interfaces (Raw + Transformed)
- Include retry with exponential backoff
- Transform all responses to camelCase with proper Date objects
- Write tests covering error scenarios
