feat(frontend): add typed API contract layer

Made-with: Cursor
This commit is contained in:
Anton
2026-04-06 16:09:28 +03:00
parent d7fb5b71ef
commit d318828f73
6 changed files with 286 additions and 2 deletions

55
frontend/src/api/http.ts Normal file
View File

@@ -0,0 +1,55 @@
import { ApiError, toApiError } from "./errors";
const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined)?.trim() || "http://localhost:3001";
function buildUrl(path: string): string {
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
return `${API_BASE_URL}${normalizedPath}`;
}
async function parseResponseBody(response: Response): Promise<unknown> {
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.includes("application/json")) {
return null;
}
try {
return await response.json();
} catch {
return null;
}
}
export async function requestJson<T>(path: string, init?: RequestInit): Promise<T> {
try {
const response = await fetch(buildUrl(path), {
...init,
headers: {
"Content-Type": "application/json",
...(init?.headers ?? {}),
},
});
if (response.status === 204) {
return undefined as T;
}
const payload = await parseResponseBody(response);
if (!response.ok) {
throw toApiError(response.status, payload);
}
return payload as T;
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new ApiError({
code: "network_error",
status: null,
message: "Не удалось связаться с сервером.",
});
}
}