w-funnel/src/shared/api/httpClient.ts
gofnnp ace03937db session
add session
2025-10-04 20:56:37 +04:00

216 lines
6.5 KiB
TypeScript

import { z } from "zod";
import { devLogger } from "../utils/logger";
import { env } from "@/lib/env";
export class ApiError extends Error {
constructor(
public status: number,
public data: unknown,
message = "API Error"
) {
super(message);
this.name = "ApiError";
}
}
type RequestOpts = Omit<RequestInit, "method" | "body"> & {
tags?: string[]; // next.js cache-tag
query?: Record<string, unknown>; // query-string
schema?: z.ZodTypeAny; // runtime validation
revalidate?: number;
skipAuthRedirect?: boolean;
};
class HttpClient {
constructor(private baseUrl: string) {}
private buildUrl(
rootUrl: string,
path: string,
query?: Record<string, unknown>
) {
const url = new URL(path, rootUrl);
if (query)
Object.entries(query).forEach(([k, v]) =>
url.searchParams.append(k, String(v))
);
return url.toString();
}
private async request<T>(
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
rootUrl: string = this.baseUrl,
path: string,
opts: RequestOpts = {},
body?: unknown,
errorMessage?: string
): Promise<T> {
const {
tags = [],
schema,
query,
revalidate = 300,
skipAuthRedirect = false,
...rest
} = opts;
const fullUrl = this.buildUrl(rootUrl, path, query);
const startTime = Date.now();
// Log API request (both client and server with ENV control)
if (typeof window !== "undefined") {
// Client-side logging
devLogger.apiRequest(fullUrl, method, body);
} else {
// Server-side logging (requires ENV variable)
if (typeof devLogger.serverApiRequest === "function") {
devLogger.serverApiRequest(fullUrl, method, body);
} else {
// Fallback server logging
if (env.DEV_LOGGER_SERVER_ENABLED === "true") {
console.group(`\n🚀 [SERVER] API REQUEST: ${method} ${fullUrl}`);
if (body !== undefined) {
console.log("📦 Request Body:", JSON.stringify(body, null, 2));
}
console.groupEnd();
}
}
}
const headers = new Headers();
let accessToken: string | undefined;
if (typeof window === "undefined") {
const { getServerAccessToken } = await import("../auth/token");
accessToken = await getServerAccessToken();
} else {
try {
const { getClientAccessToken } = await import("../auth/token");
accessToken = getClientAccessToken();
} catch {
// ignore
}
}
if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
headers.set("Content-Type", "application/json");
const res = await fetch(fullUrl, {
method,
body: body ? JSON.stringify(body) : undefined,
headers,
next: { revalidate, tags },
...rest,
});
const payload = await res.json().catch(() => null);
const duration = Date.now() - startTime;
if (!res.ok) {
// Log API error response (both client and server)
if (typeof window !== "undefined") {
devLogger.apiResponse(fullUrl, method, res.status, payload, duration);
} else {
if (typeof devLogger.serverApiResponse === "function") {
devLogger.serverApiResponse(
fullUrl,
method,
res.status,
payload,
duration
);
} else {
// Fallback server logging
if (env.DEV_LOGGER_SERVER_ENABLED === "true") {
const emoji = res.status >= 200 && res.status < 300 ? "✅" : "❌";
console.group(
`\n${emoji} [SERVER] API ERROR: ${method} ${fullUrl}`
);
console.log(`📊 Status: ${res.status}`);
console.log(`⏱️ Duration: ${duration}ms`);
if (payload !== undefined) {
console.log("📦 Error Response:", payload);
}
console.groupEnd();
}
}
}
if (res.status === 401 && !skipAuthRedirect) {
if (typeof window === "undefined") {
const { redirect } = await import("next/navigation");
redirect(env.NEXT_PUBLIC_AUTH_REDIRECT_URL || "");
} else {
const url = env.NEXT_PUBLIC_AUTH_REDIRECT_URL || "/";
window.location.href = url;
}
}
throw new ApiError(res.status, payload, errorMessage);
}
const data = payload as T;
const validatedData = schema ? (schema.parse(data) as T) : data;
// Log successful API response (both client and server)
if (typeof window !== "undefined") {
devLogger.apiResponse(
fullUrl,
method,
res.status,
validatedData,
duration
);
} else {
if (typeof devLogger.serverApiResponse === "function") {
devLogger.serverApiResponse(
fullUrl,
method,
res.status,
validatedData,
duration
);
} else {
// Fallback server logging
if (env.DEV_LOGGER_SERVER_ENABLED === "true") {
console.group(`\n✅ [SERVER] API SUCCESS: ${method} ${fullUrl}`);
console.log(`📊 Status: ${res.status}`);
console.log(`⏱️ Duration: ${duration}ms`);
if (validatedData !== undefined) {
const responsePreview =
typeof validatedData === "object" && validatedData !== null
? Array.isArray(validatedData)
? `Array[${validatedData.length}]`
: `Object{${Object.keys(validatedData)
.slice(0, 5)
.join(", ")}${
Object.keys(validatedData).length > 5 ? "..." : ""
}}`
: validatedData;
console.log("📦 Response Preview:", responsePreview);
}
console.groupEnd();
}
}
}
return validatedData;
}
get = <T>(p: string, o?: RequestOpts, u?: string) =>
this.request<T>("GET", u, p, o);
post = <T>(p: string, b: unknown, o?: RequestOpts, u?: string) =>
this.request<T>("POST", u, p, o, b);
put = <T>(p: string, b: unknown, o?: RequestOpts, u?: string) =>
this.request<T>("PUT", u, p, o, b);
patch = <T>(p: string, b: unknown, o?: RequestOpts, u?: string) =>
this.request<T>("PATCH", u, p, o, b);
delete = <T>(p: string, o?: RequestOpts, u?: string) =>
this.request<T>("DELETE", u, p, o);
}
const apiUrl = env.NEXT_PUBLIC_API_URL;
if (!apiUrl) {
throw new Error("NEXT_PUBLIC_API_URL environment variable is required");
}
export const http = new HttpClient(apiUrl);