Refactor code structure for improved readability and maintainability

This commit is contained in:
Firman Ramdhani
2026-05-22 17:53:35 +07:00
parent af0fe70ac6
commit 4260e240a0
37 changed files with 2915 additions and 74 deletions
+128
View File
@@ -0,0 +1,128 @@
import { AxiosError, type AxiosResponse } from 'axios';
import { ApiErrorCode, httpStatusToErrorCode } from './error-codes';
/**
* Structured API error that normalizes Axios errors into a
* predictable, serializable format.
*
* Replaces the legacy `ErrorRequest` class with richer metadata.
*
* @example
* ```ts
* try {
* await apiClient.get('/users');
* } catch (err) {
* if (err instanceof ApiError) {
* console.log(err.code); // ApiErrorCode.UNAUTHORIZED
* console.log(err.status); // 401
* console.log(err.data); // { message: "Token expired" }
* }
* }
* ```
*/
export class ApiError extends Error {
/** Structured error code for programmatic handling. */
readonly code: ApiErrorCode;
/** HTTP status code (0 if no response, e.g., network error). */
readonly status: number;
/** Raw response body from the server, if available. */
readonly data: unknown;
/** The original Axios error, preserved for debugging. */
readonly cause: AxiosError | undefined;
constructor(
message: string,
code: ApiErrorCode,
status: number,
data?: unknown,
cause?: AxiosError,
) {
super(message);
this.name = 'ApiError';
this.code = code;
this.status = status;
this.data = data;
this.cause = cause;
// Maintain proper prototype chain for instanceof checks
Object.setPrototypeOf(this, ApiError.prototype);
}
/**
* Factory: creates an ApiError from an AxiosError.
* Automatically resolves the error code from the HTTP status.
*/
static fromAxiosError(error: AxiosError<unknown>): ApiError {
// Network error (no response received)
if (!error.response) {
if (error.code === 'ECONNABORTED') {
return new ApiError(
'Request timed out',
ApiErrorCode.TIMEOUT,
0,
undefined,
error,
);
}
if (error.code === 'ERR_CANCELED') {
return new ApiError(
'Request was cancelled',
ApiErrorCode.CANCELLED,
0,
undefined,
error,
);
}
return new ApiError(
error.message || 'Network error',
ApiErrorCode.NETWORK_ERROR,
0,
undefined,
error,
);
}
// Server responded with an error status
const response: AxiosResponse = error.response;
const status = response.status;
const data = response.data;
const code = httpStatusToErrorCode(status);
// Extract message from common server response formats
const serverMessage =
(data && typeof data === 'object' && 'message' in data)
? String((data as Record<string, unknown>).message)
: `Request failed with status ${status}`;
return new ApiError(serverMessage, code, status, data, error);
}
/** Convenience check for authentication failures. */
get isUnauthorized(): boolean {
return this.code === ApiErrorCode.UNAUTHORIZED;
}
/** Convenience check for permission failures. */
get isForbidden(): boolean {
return this.code === ApiErrorCode.FORBIDDEN;
}
/** Convenience check for network/connectivity issues. */
get isNetworkError(): boolean {
return this.code === ApiErrorCode.NETWORK_ERROR;
}
/** JSON-serializable representation for logging/telemetry. */
toJSON(): Record<string, unknown> {
return {
name: this.name,
message: this.message,
code: this.code,
status: this.status,
data: this.data,
};
}
}
@@ -0,0 +1,42 @@
/**
* Enumeration of well-known API error codes.
*
* Use these to programmatically handle specific server responses
* without relying on magic strings scattered across the codebase.
*/
export enum ApiErrorCode {
// ─── HTTP Standard ────────────────────────────────────────────
BAD_REQUEST = 'BAD_REQUEST',
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
NOT_FOUND = 'NOT_FOUND',
CONFLICT = 'CONFLICT',
UNPROCESSABLE_ENTITY = 'UNPROCESSABLE_ENTITY',
TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS',
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
// ─── Client-side ──────────────────────────────────────────────
NETWORK_ERROR = 'NETWORK_ERROR',
TIMEOUT = 'TIMEOUT',
CANCELLED = 'CANCELLED',
UNKNOWN = 'UNKNOWN',
}
/**
* Maps HTTP status codes to ApiErrorCode enum values.
*/
export function httpStatusToErrorCode(status: number): ApiErrorCode {
switch (status) {
case 400: return ApiErrorCode.BAD_REQUEST;
case 401: return ApiErrorCode.UNAUTHORIZED;
case 403: return ApiErrorCode.FORBIDDEN;
case 404: return ApiErrorCode.NOT_FOUND;
case 409: return ApiErrorCode.CONFLICT;
case 422: return ApiErrorCode.UNPROCESSABLE_ENTITY;
case 429: return ApiErrorCode.TOO_MANY_REQUESTS;
case 500: return ApiErrorCode.INTERNAL_SERVER_ERROR;
case 503: return ApiErrorCode.SERVICE_UNAVAILABLE;
default: return ApiErrorCode.UNKNOWN;
}
}
+2
View File
@@ -0,0 +1,2 @@
export { ApiError } from './api-error';
export { ApiErrorCode, httpStatusToErrorCode } from './error-codes';