docs: add comprehensive API error codes reference for frontend consumption

Document all 33 structured errorCode values from DomainException/ErrorCode
enum across all modules (auth, user, listing, property, media, payment,
subscription, course). Includes HTTP status mapping, Vietnamese error
messages, usage examples per module, alphabetical quick-reference table,
and TypeScript integration guide for frontend error handling.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-10 20:11:12 +07:00
parent 017d85247e
commit f5ef9d8c86

441
docs/api-error-codes.md Normal file
View File

@@ -0,0 +1,441 @@
# API Error Codes Reference
All GoodGo Platform API errors follow a structured JSON format. This document lists every `errorCode` value, its HTTP status, description, and usage examples.
## Error Response Format
Every error response from the API has this shape:
```json
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Human-readable error description",
"details": { },
"correlationId": "optional-uuid",
"timestamp": "2026-04-10T12:00:00.000Z"
}
```
| Field | Type | Description |
|-----------------|----------|-------------|
| `statusCode` | `number` | HTTP status code (e.g. `400`, `401`, `404`). |
| `errorCode` | `string` | Machine-readable error identifier (see tables below). |
| `message` | `string` | Human-readable explanation (Vietnamese for business rules, English for system errors). |
| `details` | `object` | Optional structured metadata (e.g. validation field names, current/target status). |
| `correlationId` | `string` | Echoed from the `X-Correlation-Id` request header when present. |
| `timestamp` | `string` | ISO 8601 timestamp of the error occurrence. |
---
## General Error Codes
These codes apply across all modules. They are also used as fallback codes when a plain `HttpException` (without a domain error code) is thrown.
| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `INTERNAL_ERROR` | `500` | Unexpected server error. | Unhandled exceptions, infrastructure failures. |
| `VALIDATION_FAILED` | `400` | Request data failed validation. | Invalid input fields, business rule violations (e.g. invalid phone format, bad status transitions). |
| `NOT_FOUND` | `404` | Requested resource does not exist. | Entity lookup by ID returns no result. |
| `CONFLICT` | `409` | Resource state conflict. | Duplicate creation, conflicting state transition. |
| `UNAUTHORIZED` | `401` | Authentication required or failed. | Missing/invalid/expired auth token, bad credentials. |
| `FORBIDDEN` | `403` | Insufficient permissions. | Role-based access denial, CSRF token failure, resource ownership mismatch. |
| `BAD_REQUEST` | `400` | Malformed request. | Invalid file upload, unsupported content type, bad query parameters. |
| `TOO_MANY_REQUESTS` | `429` | Rate limit exceeded. | Too many API requests in a short period. |
---
## Auth Error Codes
| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `AUTH_INVALID_CREDENTIALS` | `401` | Login credentials are incorrect. | Phone number or password mismatch during local login. |
| `AUTH_TOKEN_EXPIRED` | `401` | JWT token has expired. | Access token or refresh token past expiration. |
| `AUTH_TOKEN_INVALID` | `401` | JWT token is malformed or tampered with. | Token signature verification failure. |
| `AUTH_INSUFFICIENT_PERMISSIONS` | `403` | Authenticated user lacks required role/permission. | Non-admin accessing admin endpoints, role mismatch. |
### Auth Module Usage Examples
```json
// POST /auth/login — wrong password
{
"statusCode": 401,
"errorCode": "UNAUTHORIZED",
"message": "Số điện thoại hoặc mật khẩu không đúng"
}
// POST /auth/refresh — expired refresh token
{
"statusCode": 401,
"errorCode": "UNAUTHORIZED",
"message": "Refresh token không hợp lệ hoặc đã hết hạn"
}
// POST /auth/register — phone already registered
{
"statusCode": 409,
"errorCode": "CONFLICT",
"message": "Số điện thoại đã được đăng ký"
}
// POST /auth/register — invalid phone format
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Số điện thoại không hợp lệ. Yêu cầu 10 chữ số bắt đầu bằng 0"
}
```
---
## User Error Codes
| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `USER_NOT_FOUND` | `404` | User does not exist. | Profile lookup, admin user management. |
| `USER_ALREADY_EXISTS` | `409` | User with given identifier already exists. | Duplicate registration (phone or email). |
| `USER_INVALID_PHONE` | `400` | Phone number format is invalid. | Registration or profile update with bad phone format. |
### User Module Usage Examples
```json
// GET /auth/profile — deleted or nonexistent user
{
"statusCode": 404,
"errorCode": "NOT_FOUND",
"message": "Người dùng with id '...' not found"
}
// DELETE /auth/account — account already deleted
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Tài khoản đã bị xóa"
}
```
---
## Listing Error Codes
| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `LISTING_NOT_FOUND` | `404` | Listing does not exist. | Lookup by ID for viewing, moderation, or status change. |
| `LISTING_INVALID_STATUS_TRANSITION` | `400` | Status transition is not allowed. | Attempting an invalid workflow step (e.g. `EXPIRED``ACTIVE`). |
| `LISTING_ALREADY_ACTIVE` | `409` | Listing is already in active state. | Re-activating a listing that is already published. |
| `LISTING_EXPIRED` | `400` | Listing has expired. | Attempting operations on an expired listing. |
### Listing Module Usage Examples
```json
// PATCH /listings/:id/status — invalid transition
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Không thể chuyển trạng thái từ EXPIRED sang ACTIVE",
"details": {
"currentStatus": "EXPIRED",
"targetStatus": "ACTIVE"
}
}
// POST /admin/listings/:id/approve — already approved
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Listing này không ở trạng thái chờ duyệt"
}
// POST /listings — invalid address
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Địa chỉ không hợp lệ"
}
```
---
## Property Error Codes
| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `PROPERTY_NOT_FOUND` | `404` | Property does not exist. | Property lookup for media upload or listing creation. |
### Property Module Usage Examples
```json
// POST /properties/:id/media — property not found
{
"statusCode": 404,
"errorCode": "NOT_FOUND",
"message": "Property with id '...' not found"
}
// POST /properties/:id/media — too many files
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Tối đa 30 ảnh/video cho mỗi bất động sản"
}
```
---
## Media Error Codes
| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `MEDIA_UPLOAD_FAILED` | `500` | File upload to storage provider failed. | S3/cloud storage write failure. |
| `MEDIA_LIMIT_EXCEEDED` | `400` | Maximum number of media files reached. | Exceeding per-property media limit. |
---
## Payment Error Codes
| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `PAYMENT_FAILED` | `500` | Payment processing failed. | Gateway error, provider timeout, payment link creation failure. |
| `PAYMENT_ALREADY_PROCESSED` | `409` | Payment has already been completed, failed, or refunded. | Duplicate callback, retry on non-pending payment, refunding a non-completed payment. |
| `PAYMENT_INVALID_AMOUNT` | `400` | Payment amount is invalid. | Zero/negative amount, amount below minimum threshold. |
### Payment Module Usage Examples
```json
// POST /payments — duplicate idempotency key
{
"statusCode": 409,
"errorCode": "CONFLICT",
"message": "Thanh toán với idempotency key này đã tồn tại"
}
// POST /payments/callback — invalid signature
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Chữ ký callback không hợp lệ"
}
// POST /payments/:id/refund — payment not completed
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Chỉ có thể hoàn tiền cho thanh toán đã hoàn tất"
}
// Entity-level: marking already-processed payment as completed
{
"statusCode": 409,
"errorCode": "PAYMENT_ALREADY_PROCESSED",
"message": "Cannot complete payment in status COMPLETED"
}
```
---
## Subscription Error Codes
| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `SUBSCRIPTION_NOT_FOUND` | `404` | Subscription does not exist. | Lookup for cancellation, upgrade, or usage metering. |
| `SUBSCRIPTION_ALREADY_ACTIVE` | `409` | User already has an active subscription. | Attempting to create a second active subscription. |
| `SUBSCRIPTION_ALREADY_CANCELLED` | `409` | Subscription has already been cancelled. | Re-cancelling a subscription. |
| `SUBSCRIPTION_INACTIVE` | `409` | Subscription is not in an active state. | Upgrading, expiring, or marking past-due on a non-active subscription. |
| `QUOTA_EXCEEDED` | `403` | Usage quota for the current plan has been exceeded. | Exceeding listing, lead, or search quota limits. |
### Subscription Module Usage Examples
```json
// POST /subscriptions — already has active subscription
{
"statusCode": 409,
"errorCode": "CONFLICT",
"message": "Người dùng đã có subscription đang hoạt động"
}
// POST /subscriptions/cancel — already cancelled
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Subscription đã bị hủy trước đó"
}
// POST /subscriptions/upgrade — subscription not active
{
"statusCode": 400,
"errorCode": "VALIDATION_FAILED",
"message": "Subscription không ở trạng thái hoạt động"
}
// Entity-level: upgrade on inactive subscription
{
"statusCode": 409,
"errorCode": "SUBSCRIPTION_INACTIVE",
"message": "Không thể nâng cấp subscription ở trạng thái CANCELLED"
}
// Quota guard — plan limit exceeded
{
"statusCode": 403,
"errorCode": "FORBIDDEN",
"message": "Bạn đã đạt giới hạn listings. Vui lòng nâng cấp gói để tiếp tục."
}
```
---
## Course Error Codes
| Error Code | HTTP Status | Description | When It Occurs |
|---|---|---|---|
| `COURSE_NOT_FOUND` | `404` | Course does not exist. | Course lookup by ID. |
| `COURSE_ALREADY_PUBLISHED` | `409` | Course is already in published state. | Re-publishing an active course. |
| `COURSE_ENROLLMENT_CLOSED` | `400` | Course enrollment period has ended. | Attempting to enroll after the cutoff date. |
---
## Error Code Quick-Reference (Sorted Alphabetically)
| Error Code | HTTP Status | Module |
|---|---|---|
| `AUTH_INSUFFICIENT_PERMISSIONS` | `403` | Auth |
| `AUTH_INVALID_CREDENTIALS` | `401` | Auth |
| `AUTH_TOKEN_EXPIRED` | `401` | Auth |
| `AUTH_TOKEN_INVALID` | `401` | Auth |
| `BAD_REQUEST` | `400` | General |
| `CONFLICT` | `409` | General |
| `COURSE_ALREADY_PUBLISHED` | `409` | Course |
| `COURSE_ENROLLMENT_CLOSED` | `400` | Course |
| `COURSE_NOT_FOUND` | `404` | Course |
| `FORBIDDEN` | `403` | General |
| `INTERNAL_ERROR` | `500` | General |
| `LISTING_ALREADY_ACTIVE` | `409` | Listing |
| `LISTING_EXPIRED` | `400` | Listing |
| `LISTING_INVALID_STATUS_TRANSITION` | `400` | Listing |
| `LISTING_NOT_FOUND` | `404` | Listing |
| `MEDIA_LIMIT_EXCEEDED` | `400` | Media |
| `MEDIA_UPLOAD_FAILED` | `500` | Media |
| `NOT_FOUND` | `404` | General |
| `PAYMENT_ALREADY_PROCESSED` | `409` | Payment |
| `PAYMENT_FAILED` | `500` | Payment |
| `PAYMENT_INVALID_AMOUNT` | `400` | Payment |
| `PROPERTY_NOT_FOUND` | `404` | Property |
| `QUOTA_EXCEEDED` | `403` | Subscription |
| `SUBSCRIPTION_ALREADY_ACTIVE` | `409` | Subscription |
| `SUBSCRIPTION_ALREADY_CANCELLED` | `409` | Subscription |
| `SUBSCRIPTION_INACTIVE` | `409` | Subscription |
| `SUBSCRIPTION_NOT_FOUND` | `404` | Subscription |
| `TOO_MANY_REQUESTS` | `429` | General |
| `UNAUTHORIZED` | `401` | General |
| `USER_ALREADY_EXISTS` | `409` | User |
| `USER_INVALID_PHONE` | `400` | User |
| `USER_NOT_FOUND` | `404` | User |
---
## Frontend Integration Guide
### TypeScript Error Type
```typescript
interface ApiError {
statusCode: number;
errorCode: string;
message: string;
details?: Record<string, unknown>;
correlationId?: string;
timestamp: string;
}
```
### Handling Errors by Code
```typescript
import axios, { AxiosError } from 'axios';
async function handleApiCall() {
try {
const res = await axios.post('/api/listings', payload);
return res.data;
} catch (err) {
if (axios.isAxiosError(err)) {
const apiError = err.response?.data as ApiError;
switch (apiError.errorCode) {
case 'VALIDATION_FAILED':
// Show field-level errors from apiError.details
showValidationErrors(apiError.details);
break;
case 'UNAUTHORIZED':
case 'AUTH_TOKEN_EXPIRED':
// Redirect to login or refresh token
await refreshSession();
break;
case 'QUOTA_EXCEEDED':
// Show upgrade prompt
showUpgradeDialog(apiError.message);
break;
case 'CONFLICT':
// Inform user of duplicate action
showToast(apiError.message, 'warning');
break;
case 'NOT_FOUND':
// Navigate to 404 page or show missing resource message
router.push('/404');
break;
default:
showToast('Đã xảy ra lỗi, vui lòng thử lại', 'error');
}
}
}
}
```
### Error Code Constants (Optional)
For type safety, maintain error codes as a union type on the frontend:
```typescript
type ErrorCode =
| 'INTERNAL_ERROR'
| 'VALIDATION_FAILED'
| 'NOT_FOUND'
| 'CONFLICT'
| 'UNAUTHORIZED'
| 'FORBIDDEN'
| 'BAD_REQUEST'
| 'TOO_MANY_REQUESTS'
| 'AUTH_INVALID_CREDENTIALS'
| 'AUTH_TOKEN_EXPIRED'
| 'AUTH_TOKEN_INVALID'
| 'AUTH_INSUFFICIENT_PERMISSIONS'
| 'USER_NOT_FOUND'
| 'USER_ALREADY_EXISTS'
| 'USER_INVALID_PHONE'
| 'COURSE_NOT_FOUND'
| 'COURSE_ALREADY_PUBLISHED'
| 'COURSE_ENROLLMENT_CLOSED'
| 'LISTING_NOT_FOUND'
| 'LISTING_INVALID_STATUS_TRANSITION'
| 'LISTING_ALREADY_ACTIVE'
| 'LISTING_EXPIRED'
| 'PROPERTY_NOT_FOUND'
| 'MEDIA_UPLOAD_FAILED'
| 'MEDIA_LIMIT_EXCEEDED'
| 'PAYMENT_FAILED'
| 'PAYMENT_ALREADY_PROCESSED'
| 'PAYMENT_INVALID_AMOUNT'
| 'SUBSCRIPTION_NOT_FOUND'
| 'SUBSCRIPTION_ALREADY_ACTIVE'
| 'SUBSCRIPTION_ALREADY_CANCELLED'
| 'SUBSCRIPTION_INACTIVE'
| 'QUOTA_EXCEEDED';
```
---
## Source Files
- **Error code enum**: `apps/api/src/modules/shared/domain/error-codes.ts`
- **Domain exceptions**: `apps/api/src/modules/shared/domain/domain-exception.ts`
- **Global exception filter**: `apps/api/src/modules/shared/infrastructure/filters/global-exception.filter.ts`