Files
lcbp3/specs/05-decisions/ADR-007-api-design-error-handling.md
admin 047e1b88ce
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled
Main: revise specs to 1.5.0 (completed)
2025-12-01 01:28:32 +07:00

10 KiB

ADR-007: API Design & Error Handling Strategy

Status: Accepted Date: 2025-12-01 Decision Makers: Backend Team, System Architect Related Documents: Backend Guidelines, ADR-005: Technology Stack


Context and Problem Statement

ระบบ LCBP3-DMS ต้องการมาตรฐานการออกแบบ API ที่ชัดเจนและสม่ำเสมอทั้งระบบ รวมถึงกลยุทธ์การจัดการ Error และ Validation ที่เหมาะสม

ปัญหาที่ต้องแก้:

  1. API Consistency: ทำอย่างไรให้ API response format สม่ำเสมอทั้งระบบ
  2. Error Handling: จัดการ error อย่างไรให้ client เข้าใจและแก้ไขได้
  3. Validation: Validate request อย่างไรให้ครอบคลุมและให้ feedback ที่ดี
  4. Status Codes: ใช้ HTTP status codes อย่างไรให้ถูกต้องและสม่ำเสมอ

Decision Drivers

  • 🎯 Developer Experience: Frontend developers ต้องใช้ API ได้ง่าย
  • 🔒 Security: ป้องกัน Information Leakage จาก Error messages
  • 📊 Debuggability: ต้องหา Root cause ของ Error ได้ง่าย
  • 🌍 Internationalization: รองรับภาษาไทยและอังกฤษ
  • 📝 Standards Compliance: ใช้มาตรฐานที่เป็นที่ยอมรับ (REST, JSON:API)

Considered Options

Option 1: Standard REST with Custom Error Format

รูปแบบ:

// Success
{
  "data": { ... },
  "meta": { "timestamp": "..." }
}

// Error
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [...]
  }
}

Pros:

  • Simple และเข้าใจง่าย
  • Flexible สำหรับ Custom needs
  • ไม่ต้อง Follow spec ที่ซับซ้อน

Cons:

  • ไม่มี Standard specification
  • ต้องสื่อสารภายในทีมให้ชัดเจน
  • อาจไม่สม่ำเสมอหากไม่ระวัง

Option 2: JSON:API Specification

รูปแบบ:

{
  "data": {
    "type": "correspondences",
    "id": "1",
    "attributes": { ... },
    "relationships": { ... }
  },
  "included": [...]
}

Pros:

  • มาตรฐานที่เป็นที่ยอมรับ
  • มี Libraries ช่วย
  • รองรับ Relationships ได้ดี

Cons:

  • ซับซ้อนเกินความจำเป็น
  • Verbose (ข้อมูลซ้ำซ้อน)
  • Learning curve สูง

Option 3: GraphQL

Pros:

  • Client เลือกข้อมูลที่ต้องการได้
  • ลด Over-fetching/Under-fetching
  • Strong typing

Cons:

  • Complexity สูง
  • Caching ยาก
  • ไม่เหมาะกับ Document-heavy system
  • Team ยังไม่มีประสบการณ์

Decision Outcome

Chosen Option: Option 1 - Standard REST with Custom Error Format + NestJS Exception Filters

Rationale

  1. Simplicity: ทีมคุ้นเคยกับ REST API และ NestJS มี Built-in support ที่ดี
  2. Flexibility: สามารถปรับแต่งตาม Business needs ได้ง่าย
  3. Performance: Lightweight กว่า JSON:API และ GraphQL
  4. Team Capability: ทีมมีประสบการณ์ REST มากกว่า GraphQL

Implementation Details

1. Success Response Format

// Single resource
{
  "data": {
    "id": 1,
    "document_number": "CORR-2024-0001",
    "subject": "...",
    ...
  },
  "meta": {
    "timestamp": "2024-01-01T00:00:00Z",
    "version": "1.0"
  }
}

// Collection with pagination
{
  "data": [
    { "id": 1, ... },
    { "id": 2, ... }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 100,
      "totalPages": 5
    },
    "timestamp": "2024-01-01T00:00:00Z"
  }
}

2. Error Response Format

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed on input data",
    "statusCode": 400,
    "timestamp": "2024-01-01T00:00:00Z",
    "path": "/api/correspondences",
    "details": [
      {
        "field": "subject",
        "message": "Subject is required",
        "value": null
      }
    ]
  }
}

3. HTTP Status Codes

Status Use Case
200 OK Successful GET, PUT, PATCH
201 Created Successful POST
204 No Content Successful DELETE
400 Bad Request Validation error, Invalid input
401 Unauthorized Missing or invalid JWT token
403 Forbidden Insufficient permissions (RBAC)
404 Not Found Resource not found
409 Conflict Duplicate resource, Business rule violation
422 Unprocessable Entity Business logic error
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Unexpected server error

4. Global Exception Filter

// File: backend/src/common/filters/global-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let code = 'INTERNAL_SERVER_ERROR';
    let message = 'An unexpected error occurred';
    let details = null;

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const exceptionResponse = exception.getResponse();

      if (typeof exceptionResponse === 'object') {
        code = (exceptionResponse as any).error || exception.name;
        message = (exceptionResponse as any).message || exception.message;
        details = (exceptionResponse as any).details;
      } else {
        message = exceptionResponse;
      }
    }

    // Log error (but don't expose internal details to client)
    console.error('Exception:', exception);

    response.status(status).json({
      error: {
        code,
        message,
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
        ...(details && { details }),
      },
    });
  }
}

5. Custom Business Exception

// File: backend/src/common/exceptions/business.exception.ts
export class BusinessException extends HttpException {
  constructor(message: string, code: string = 'BUSINESS_ERROR') {
    super(
      {
        error: code,
        message,
      },
      HttpStatus.UNPROCESSABLE_ENTITY
    );
  }
}

// Usage
throw new BusinessException(
  'Cannot approve correspondence in current status',
  'INVALID_WORKFLOW_TRANSITION'
);

6. Validation Pipe Configuration

// File: backend/src/main.ts
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true, // Strip properties not in DTO
    forbidNonWhitelisted: true, // Throw error if unknown properties
    transform: true, // Auto-transform payloads to DTO instances
    transformOptions: {
      enableImplicitConversion: true,
    },
    exceptionFactory: (errors) => {
      const details = errors.map((error) => ({
        field: error.property,
        message: Object.values(error.constraints || {}).join(', '),
        value: error.value,
      }));

      return new HttpException(
        {
          error: 'VALIDATION_ERROR',
          message: 'Validation failed',
          details,
        },
        HttpStatus.BAD_REQUEST
      );
    },
  })
);

Consequences

Positive Consequences

  1. Consistency: API responses มีรูปแบบสม่ำเสมอทั้งระบบ
  2. Developer Friendly: Frontend developers ใช้งาน API ได้ง่าย
  3. Debuggability: Error messages ให้ข้อมูลเพียงพอสำหรับ Debug
  4. Security: ไม่เปิดเผย Internal error details ให้ Client
  5. Maintainability: ใช้ NestJS built-in features ทำให้ Maintain ง่าย

Negative Consequences

  1. No Standard Spec: ไม่ใช่ Standard เช่น JSON:API จึงต้องเขียน Documentation ชัดเจน
  2. Manual Documentation: ต้อง Document API response format เอง
  3. Learning Curve: Team members ใหม่ต้องเรียนรู้ Error code conventions

Mitigation Strategies

  • Documentation: ใช้ Swagger/OpenAPI เพื่อ Auto-generate API docs
  • Code Generation: Generate TypeScript interfaces สำหรับ Frontend จาก DTOs
  • Error Code Registry: มี Centralized list ของ Error codes พร้อมคำอธิบาย
  • Testing: เขียน Integration tests เพื่อ Validate response formats


References


Last Updated: 2025-12-01 Next Review: 2025-06-01