216 lines
6.5 KiB
TypeScript
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);
|