10 KiB
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 ที่เหมาะสม
ปัญหาที่ต้องแก้:
- API Consistency: ทำอย่างไรให้ API response format สม่ำเสมอทั้งระบบ
- Error Handling: จัดการ error อย่างไรให้ client เข้าใจและแก้ไขได้
- Validation: Validate request อย่างไรให้ครอบคลุมและให้ feedback ที่ดี
- 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
- Simplicity: ทีมคุ้นเคยกับ REST API และ NestJS มี Built-in support ที่ดี
- Flexibility: สามารถปรับแต่งตาม Business needs ได้ง่าย
- Performance: Lightweight กว่า JSON:API และ GraphQL
- 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
- ✅ Consistency: API responses มีรูปแบบสม่ำเสมอทั้งระบบ
- ✅ Developer Friendly: Frontend developers ใช้งาน API ได้ง่าย
- ✅ Debuggability: Error messages ให้ข้อมูลเพียงพอสำหรับ Debug
- ✅ Security: ไม่เปิดเผย Internal error details ให้ Client
- ✅ Maintainability: ใช้ NestJS built-in features ทำให้ Maintain ง่าย
Negative Consequences
- ❌ No Standard Spec: ไม่ใช่ Standard เช่น JSON:API จึงต้องเขียน Documentation ชัดเจน
- ❌ Manual Documentation: ต้อง Document API response format เอง
- ❌ 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
Related ADRs
- ADR-005: Technology Stack - เลือกใช้ NestJS
- ADR-004: RBAC Implementation - Error 403 Forbidden
References
Last Updated: 2025-12-01 Next Review: 2025-06-01