/* eslint-disable no-console */ import { redirect } from "next/navigation"; import { z } from "zod"; import { getServerAccessToken } from "../auth/token"; import { devLogger } from "../utils/logger"; export class ApiError extends Error { constructor( public status: number, public data: unknown, message = "API Error" ) { super(message); this.name = "ApiError"; } } type RequestOpts = Omit & { tags?: string[]; // next.js cache-tag query?: Record; // 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 ) { 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( method: "GET" | "POST" | "PATCH" | "DELETE", rootUrl: string = this.baseUrl, path: string, opts: RequestOpts = {}, body?: unknown, errorMessage?: string ): Promise { 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 (process.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(); const accessToken = await getServerAccessToken(); 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 (process.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) { redirect(process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL || ""); } throw new ApiError(res.status, payload, errorMessage); } const data = payload as T; const validatedData = schema ? schema.parse(data) : 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 (process.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 = (p: string, o?: RequestOpts, u?: string) => this.request("GET", u, p, o); post = (p: string, b: unknown, o?: RequestOpts, u?: string) => this.request("POST", u, p, o, b); patch = (p: string, b: unknown, o?: RequestOpts, u?: string) => this.request("PATCH", u, p, o, b); delete = (p: string, o?: RequestOpts, u?: string) => this.request("DELETE", u, p, o); } const apiUrl = process.env.NEXT_PUBLIC_API_URL; if (!apiUrl) { throw new Error("NEXT_PUBLIC_API_URL environment variable is required"); } export const http = new HttpClient(apiUrl);